diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 0ab358c..0000000 --- a/.codecov.yml +++ /dev/null @@ -1,122 +0,0 @@ -coverage: - status: - project: - default: - threshold: 2% - -comment: - layout: "header, diff, flags, components, files" - after_n_builds: 2 - -ignore: - - "*_easyjson.go" - - "**/*_easyjson.go" - - "*.pb.go" - - "**/*.pb.go" - -component_management: - individual_components: - - component_id: module_root - name: root - paths: - - "*.go" - - component_id: module_api - name: api - paths: - - api/** - - component_id: module_async - name: async - paths: - - async/** - - component_id: module_client - name: client - paths: - - client/** - - component_id: module_cmd_client - name: cmd/client - paths: - - cmd/client/** - - component_id: module_cmd_proxy - name: cmd/proxy - paths: - - cmd/proxy/** - - component_id: module_cmd_server - name: cmd/server - paths: - - cmd/server/** - - component_id: module_config - name: config - paths: - - config/** - - component_id: module_container - name: container - paths: - - container/** - - component_id: module_dns - name: dns - paths: - - dns/** - - component_id: module_etcd - name: etcd - paths: - - etcd/** - - component_id: module_geoip - name: geoip - paths: - - geoip/** - - component_id: module_grpc - name: grpc - paths: - - grpc/** - - component_id: module_internal - name: internal - paths: - - internal/** - - component_id: module_log - name: log - paths: - - log/** - - component_id: module_metrics - name: metrics - paths: - - metrics/** - - component_id: module_mock - name: mock - paths: - - mock/** - - component_id: module_nats - name: nats - paths: - - nats/** - - component_id: module_pool - name: pool - paths: - - pool/** - - component_id: module_proxy - name: proxy - paths: - - proxy/** - - component_id: module_security - name: security - paths: - - security/** - - component_id: module_server - name: server - paths: - - server/** - - component_id: module_session - name: session - paths: - - session/** - - component_id: module_sfu - name: sfu - paths: - - sfu/** - - component_id: module_talk - name: talk - paths: - - talk/** - - component_id: module_test - name: test - paths: - - test/** diff --git a/.github/workflows/check-continentmap.yml b/.github/workflows/check-continentmap.yml index 3c26ed2..eff3c10 100644 --- a/.github/workflows/check-continentmap.yml +++ b/.github/workflows/check-continentmap.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Check continentmap run: make check-continentmap diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c65e540..045b013 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,15 +36,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v4 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/command-rebase.yml b/.github/workflows/command-rebase.yml index 489574b..7fd90cd 100644 --- a/.github/workflows/command-rebase.yml +++ b/.github/workflows/command-rebase.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Add reaction on start - uses: peter-evans/create-or-update-comment@v5 + uses: peter-evans/create-or-update-comment@v4 with: token: ${{ secrets.COMMAND_BOT_PAT }} repository: ${{ github.event.repository.full_name }} @@ -31,7 +31,7 @@ jobs: reaction-type: "+1" - name: Checkout the latest code - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.COMMAND_BOT_PAT }} @@ -42,7 +42,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} - name: Add reaction on failure - uses: peter-evans/create-or-update-comment@v5 + uses: peter-evans/create-or-update-comment@v4 if: failure() with: token: ${{ secrets.COMMAND_BOT_PAT }} diff --git a/.github/workflows/deploydocker.yml b/.github/workflows/deploydocker.yml index 411e59a..2471061 100644 --- a/.github/workflows/deploydocker.yml +++ b/.github/workflows/deploydocker.yml @@ -1,4 +1,4 @@ -name: Deploy to Docker Hub / GHCR / quay.io +name: Deploy to Docker Hub / GHCR on: pull_request: @@ -27,19 +27,18 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@v3 - name: Generate Docker metadata id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@v5 with: images: | strukturag/nextcloud-spreed-signaling ghcr.io/strukturag/nextcloud-spreed-signaling - quay.io/strukturag/nextcloud-spreed-signaling tags: | type=ref,event=branch type=semver,pattern={{version}} @@ -47,7 +46,7 @@ jobs: type=semver,pattern={{major}} type=sha,prefix= - name: Cache Docker layers - uses: actions/cache@v5 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} @@ -55,34 +54,26 @@ jobs: ${{ runner.os }}-buildx- - name: Login to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GHCR if: github.event_name != 'pull_request' - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to quay.io - if: github.event_name != 'pull_request' - uses: docker/login-action@v4 - with: - registry: quay.io - username: ${{ secrets.QUAY_IO_USERNAME }} - password: ${{ secrets.QUAY_IO_ACCESS_TOKEN }} - - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - name: Build and push id: docker_build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v6 with: context: . file: ./docker/server/Dockerfile @@ -101,19 +92,18 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@v3 - name: Generate Docker metadata id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@v5 with: images: | strukturag/nextcloud-spreed-signaling ghcr.io/strukturag/nextcloud-spreed-signaling - quay.io/strukturag/nextcloud-spreed-signaling labels: | org.opencontainers.image.title=nextcloud-spreed-signaling-proxy org.opencontainers.image.description=Signaling proxy for the standalone signaling server for Nextcloud Talk. @@ -126,7 +116,7 @@ jobs: type=semver,pattern={{major}} type=sha,prefix= - name: Cache Docker layers - uses: actions/cache@v5 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} @@ -134,34 +124,26 @@ jobs: ${{ runner.os }}-buildx- - name: Login to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GHCR if: github.event_name != 'pull_request' - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to quay.io - if: github.event_name != 'pull_request' - uses: docker/login-action@v4 - with: - registry: quay.io - username: ${{ secrets.QUAY_IO_USERNAME }} - password: ${{ secrets.QUAY_IO_ACCESS_TOKEN }} - - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - name: Build and push id: docker_build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v6 with: context: . file: ./docker/proxy/Dockerfile diff --git a/.github/workflows/docker-compose.yml b/.github/workflows/docker-compose.yml index 3cc8019..5f6ae64 100644 --- a/.github/workflows/docker-compose.yml +++ b/.github/workflows/docker-compose.yml @@ -22,16 +22,26 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 + + - name: Update docker-compose + run: | + curl -SL https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-linux-x86_64 -o docker-compose + chmod a+x docker-compose - name: Pull Docker images - run: docker compose -f docker/docker-compose.yml pull + run: ./docker-compose -f docker/docker-compose.yml pull build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 + + - name: Update docker-compose + run: | + curl -SL https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-linux-x86_64 -o docker-compose + chmod a+x docker-compose - name: Build Docker images - run: docker compose -f docker/docker-compose.yml build + run: ./docker-compose -f docker/docker-compose.yml build diff --git a/.github/workflows/docker-janus.yml b/.github/workflows/docker-janus.yml index 3869d51..d96888e 100644 --- a/.github/workflows/docker-janus.yml +++ b/.github/workflows/docker-janus.yml @@ -23,13 +23,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - name: Build Docker image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v6 with: context: docker/janus load: true diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2edce0a..9b75474 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -33,16 +33,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - name: Build Docker image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v6 with: context: . file: docker/server/Dockerfile @@ -52,16 +52,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - name: Build Docker image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v6 with: context: . file: docker/proxy/Dockerfile diff --git a/.github/workflows/generated.yml b/.github/workflows/generated.yml index b649a23..8a19e1b 100644 --- a/.github/workflows/generated.yml +++ b/.github/workflows/generated.yml @@ -5,24 +5,20 @@ on: branches: [ master ] paths: - '.github/workflows/generated.yml' - - '**/api*.go' - - '**/*_easyjson.go' - - '**/*.pb.go' - - '**/*.proto' + - 'api*.go' + - '*_easyjson.go' + - '*.pb.go' + - '*.proto' - 'go.*' - - 'api/signaling.go' - - 'talk/ocs.go' pull_request: branches: [ master ] paths: - '.github/workflows/generated.yml' - - '**/api*.go' - - '**/*_easyjson.go' - - '**/*.pb.go' - - '**/*.proto' + - 'api*.go' + - '*_easyjson.go' + - '*.pb.go' + - '*.proto' - 'go.*' - - 'api/signaling.go' - - 'talk/ocs.go' env: CODE_GENERATOR_NAME: struktur AG service user @@ -57,12 +53,12 @@ jobs: contents: write continue-on-error: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 with: token: ${{ secrets.CODE_GENERATOR_PAT }} ref: ${{ github.event.pull_request.head.ref }} - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v5 with: go-version: "stable" @@ -82,15 +78,16 @@ jobs: if [ "$CHECKOUT_SHA" != "${{github.event.pull_request.head.sha}}" ]; then echo "More changes since this commit ${{github.event.pull_request.head.sha}}, skipping" else - git add --all + git add *_easyjson.go *.pb.go CHANGES=$(git status --porcelain) if [ -z "$CHANGES" ]; then echo "No files have changed, no need to commit / push." else go mod tidy + git add go.* git config user.name "$CODE_GENERATOR_NAME" git config user.email "$CODE_GENERATOR_EMAIL" - git commit --all --author="$(git log -n 1 --pretty=format:%an) <$(git log -n 1 --pretty=format:%ae)>" -m "Update generated files from ${{github.event.pull_request.head.sha}}" + git commit --author="$(git log -n 1 --pretty=format:%an) <$(git log -n 1 --pretty=format:%ae)>" -m "Update generated files from ${{github.event.pull_request.head.sha}}" git push fi fi @@ -100,8 +97,8 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version: "stable" @@ -116,5 +113,5 @@ jobs: - name: Check generated files run: | - git add --all - git diff --cached --exit-code + git add *.go + git diff --cached --exit-code *.go diff --git a/.github/workflows/govuln.yml b/.github/workflows/govuln.yml index e0047fa..a50bc62 100644 --- a/.github/workflows/govuln.yml +++ b/.github/workflows/govuln.yml @@ -24,14 +24,13 @@ jobs: strategy: matrix: go-version: - - "1.25" - - "1.26" + - "1.22" + - "1.23" steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - check-latest: true - run: date diff --git a/.github/workflows/licensecheck.yml b/.github/workflows/licensecheck.yml index 55f52a3..eec2df3 100644 --- a/.github/workflows/licensecheck.yml +++ b/.github/workflows/licensecheck.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Install licensecheck run: | @@ -33,7 +33,7 @@ jobs: run: | { echo 'CHECK_RESULT<> "$GITHUB_ENV" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1e69405..1856787 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,6 @@ on: - '.golangci.yml' - '**.go' - 'go.*' - - 'Makefile' pull_request: branches: [ master ] paths: @@ -16,7 +15,6 @@ on: - '.golangci.yml' - '**.go' - 'go.*' - - 'Makefile' permissions: contents: read @@ -27,60 +25,31 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: "1.22" - name: lint - uses: golangci/golangci-lint-action@v9.2.0 + uses: golangci/golangci-lint-action@v6.2.0 with: version: latest args: --timeout=2m0s skip-cache: true - modernize: - name: modernize - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" - - - name: moderize - run: | - go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -any=false -reflecttypefor=false -test ./... - - checklocks: - name: checklocks - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" - check-latest: true - - - name: checklocks - run: | - make checklocks - dependencies: name: dependencies runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version: "stable" - name: Check minimum supported version of Go run: | - go mod tidy -go=1.25.0 -compat=1.25.0 + go mod tidy -go=1.22.0 -compat=1.22.0 - name: Check go.mod / go.sum run: | diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 31954f7..7cadc02 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -20,7 +20,7 @@ jobs: name: shellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: shellcheck run: | diff --git a/.github/workflows/tarball.yml b/.github/workflows/tarball.yml index 5a19f10..747254a 100644 --- a/.github/workflows/tarball.yml +++ b/.github/workflows/tarball.yml @@ -24,12 +24,12 @@ jobs: strategy: matrix: go-version: - - "1.25" - - "1.26" + - "1.22" + - "1.23" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} @@ -39,71 +39,26 @@ jobs: make tarball - name: Upload tarball - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: name: tarball-${{ matrix.go-version }} path: nextcloud-spreed-signaling*.tar.gz - build: - strategy: - matrix: - go-version: - - "1.25" - - "1.26" - runs-on: ubuntu-latest - needs: [create] - steps: - - uses: actions/setup-go@v6 - with: - go-version: ${{ matrix.go-version }} - - - name: Download tarball - uses: actions/download-artifact@v8 - with: - name: tarball-${{ matrix.go-version }} - - - name: Extract tarball - run: | - mkdir -p tmp - tar xvf nextcloud-spreed-signaling*.tar.gz --strip-components=1 -C tmp - [ -d "tmp/vendor" ] || exit 1 - [ -f "tmp/version.txt" ] || exit 1 - - - name: Build - run: | - echo "Building with $(nproc) threads" - make -C tmp build client -j$(nproc) - UNKNOWN=$(./tmp/bin/signaling -version | grep unknown || true) - if [ -n "$UNKNOWN" ]; then \ - echo "Found unknown version: $UNKNOWN"; \ - exit 1; \ - fi - UNKNOWN=$(./tmp/bin/proxy -version | grep unknown || true) - if [ -n "$UNKNOWN" ]; then \ - echo "Found unknown version: $UNKNOWN"; \ - exit 1; \ - fi - UNKNOWN=$(./tmp/bin/client -version | grep unknown || true) - if [ -n "$UNKNOWN" ]; then \ - echo "Found unknown version: $UNKNOWN"; \ - exit 1; \ - fi - test: strategy: matrix: go-version: - - "1.25" - - "1.26" + - "1.22" + - "1.23" runs-on: ubuntu-latest needs: [create] steps: - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Download tarball - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v4 with: name: tarball-${{ matrix.go-version }} @@ -113,6 +68,11 @@ jobs: tar xvf nextcloud-spreed-signaling*.tar.gz --strip-components=1 -C tmp [ -d "tmp/vendor" ] || exit 1 + - name: Build + run: | + echo "Building with $(nproc) threads" + make -C tmp build -j$(nproc) + - name: Run tests env: USE_DB_IP_GEOIP_DATABASE: "1" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2dff4ac..9c74638 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,6 @@ on: branches: [ master ] paths: - '.github/workflows/test.yml' - - '.codecov.yml' - '**.go' - 'go.*' - 'Makefile' @@ -13,7 +12,6 @@ on: branches: [ master ] paths: - '.github/workflows/test.yml' - - '.codecov.yml' - '**.go' - 'go.*' - 'Makefile' @@ -22,16 +20,19 @@ permissions: contents: read jobs: - build: + go: + env: + MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }} + USE_DB_IP_GEOIP_DATABASE: "1" strategy: matrix: go-version: - - "1.25" - - "1.26" + - "1.22" + - "1.23" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} @@ -42,69 +43,38 @@ jobs: make proxy -j$(nproc) make server -j$(nproc) - go: - env: - MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }} - USE_DB_IP_GEOIP_DATABASE: "1" - strategy: - matrix: - go-version: - - "1.25" - - "1.26" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: ${{ matrix.go-version }} - - name: Run tests run: | make test TIMEOUT=120s - benchmark: - env: - MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }} - USE_DB_IP_GEOIP_DATABASE: "1" - strategy: - matrix: - go-version: - - "1.25" - - "1.26" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: ${{ matrix.go-version }} - - - name: Run benchmarks - run: | - make benchmark - - coverage: - env: - MAXMIND_GEOLITE2_LICENSE: ${{ secrets.MAXMIND_GEOLITE2_LICENSE }} - USE_DB_IP_GEOIP_DATABASE: "1" - strategy: - matrix: - go-version: - - "1.25" - - "1.26" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: ${{ matrix.go-version }} - - name: Generate coverage report run: | make cover TIMEOUT=120s + echo "GOROOT=$(go env GOROOT)" >> $GITHUB_ENV - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 + - name: Convert coverage to lcov + uses: jandelgado/gcov2lcov-action@v1.1.1 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./cover.out - flags: go-${{ matrix.go-version }} + infile: cover.out + outfile: cover.lcov + + - name: Coveralls Parallel + uses: coverallsapp/github-action@v2.3.4 + env: + COVERALLS_FLAG_NAME: run-${{ matrix.go-version }} + with: + path-to-lcov: cover.lcov + github-token: ${{ secrets.github_token }} + parallel: true + + finish: + permissions: + contents: none + needs: go + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v2.3.4 + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.gitignore b/.gitignore index 3ba4e09..680cc06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ bin/ -tmp/ vendor/ *.pem diff --git a/.golangci.yml b/.golangci.yml index c7c039b..c62551d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,80 +1,34 @@ -version: "2" linters: - enable: - - errchkjson - - exptostd - - gocritic - - misspell - - modernize - - paralleltest - - perfsprint - - revive - - testifylint - settings: - errchkjson: - check-error-free-encoding: true - report-no-exported: true - gocritic: - disabled-checks: - - singleCaseSwitch - settings: - ifElseChain: - # Min number of if-else blocks that makes the warning trigger. - # Default: 2 - minThreshold: 3 - govet: - enable: - - nilness - disable: - - stdversion - revive: - severity: warning - rules: - - name: blank-imports - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - - name: error-return - #- name: error-strings - - name: error-naming - - name: exported - - name: if-return - - name: increment-decrement - #- name: var-naming - - name: var-declaration - - name: package-comments - - name: range - - name: receiver-naming - - name: time-naming - - name: unexported-return - #- name: indent-error-flow - - name: errorf - - name: empty-block - - name: superfluous-else - #- name: unused-parameter - - name: unreachable-code - - name: use-any - - name: redefines-builtin-id - testifylint: - disable: - - require-error - exclusions: - generated: lax - presets: - - comments - - common-false-positives - - legacy - - std-error-handling - paths: - - third_party$ - - builtin$ - - examples$ -formatters: enable: - gofmt - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ + - revive + +linters-settings: + revive: + ignoreGeneratedHeader: true + severity: warning + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + #- name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + #- name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + #- name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + #- name: unused-parameter + - name: unreachable-code + - name: redefines-builtin-id diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a48ef5..7f67415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,565 +2,6 @@ All notable changes to this project will be documented in this file. -## 2.1.1 - 2026-03-12 - -### Changed -- Drop support for Golang 1.24 - [#1197](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1197) -- Simplify error type checks. - [#1190](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1190) -- readme: Add example websocket urls for Janus events. - [#1191](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1191) -- CI: Test with Golang 1.26 - [#1196](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1196) -- CI: Use "docker compose" instead of downloading docker-compose binary. - [#1203](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1203) -- docker: pin spreedbackend uid and add user group - [#1202](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1202) -- Bump module version to "v2" so versions 2.x can be imported by others. - [#1211](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1211) -- Update generated files for v2 module. - [#1218](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1218) -- Simplify async notifier code - [#1220](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1220) - -### Fixed -- Update "go.opentelemetry.io/otel/sdk" to fix "GO-2026-4394". - [#1207](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1207) -- Don't limit size of received Janus events. - [#1208](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1208) - -### Dependencies -- Bump google.golang.org/grpc/cmd/protoc-gen-go-grpc from 1.6.0 to 1.6.1 - [#1189](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1189) -- Bump markdown from 3.10.1 to 3.10.2 in /docs - [#1192](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1192) -- Bump github.com/pion/dtls/v3 from 3.0.10 to 3.1.0 - [#1193](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1193) -- Bump golang from 1.25-alpine to 1.26-alpine in /docker/proxy - [#1194](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1194) -- Bump golang from 1.25-alpine to 1.26-alpine in /docker/server - [#1195](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1195) -- Bump google.golang.org/grpc from 1.78.0 to 1.79.1 - [#1201](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1201) -- Bump github.com/pion/dtls/v3 from 3.1.0 to 3.1.1 - [#1199](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1199) -- Bump the etcd group with 4 updates - [#1200](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1200) -- Bump github.com/pion/sdp/v3 from 3.0.17 to 3.0.18 - [#1204](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1204) -- Bump github.com/pion/ice/v4 from 4.2.0 to 4.2.1 - [#1205](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1205) -- Bump github.com/nats-io/nats.go from 1.48.0 to 1.49.0 - [#1206](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1206) -- Bump the artifacts group with 2 updates - [#1209](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1209) -- Bump module version to "v2" so versions 2.x can be imported by others. - [#1211](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1211) -- Bump docker/login-action from 3 to 4 - [#1212](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1212) -- Bump docker/setup-qemu-action from 3 to 4 - [#1213](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1213) -- Bump docker/metadata-action from 5 to 6 - [#1214](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1214) -- Bump docker/setup-buildx-action from 3 to 4 - [#1215](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1215) -- Bump docker/build-push-action from 6 to 7 - [#1217](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1217) -- Bump google.golang.org/grpc from 1.79.1 to 1.79.2 - [#1216](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1216) -- Bump github.com/nats-io/nats-server/v2 from 2.12.4 to 2.12.5 - [#1219](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1219) - - -## 2.1.0 - 2026-02-03 - -### Added -- Introduce "internal" package - [#1082](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1082) -- Add etcd TLS tests. - [#1084](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1084) -- Add missing stats registration. - [#1086](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1086) -- Add commands to the readme on how to build Docker images locally. - [#1088](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1088) -- Use gvisor checklocks for static lock analysis. - [#1078](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1078) -- Support relaying of chat messages. - [#868](https://github.com/strukturag/nextcloud-spreed-signaling/pull/868) -- Return bandwidth information in room responses. - [#1099](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1099) -- Expose real bandwidth usage through metrics. - [#1102](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1102) -- Add type to store bandwidths. - [#1108](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1108) -- Add more WebRTC-related metrics - [#1109](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1109) -- Add metrics about client bytes/messages sent/received. - [#1134](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1134) -- Introduce transient session data. - [#1120](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1120) -- Include "version.txt" in tarball. - [#1142](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1142) -- CI: Also upload images to quay.io/strukturag/nextcloud-spreed-signaling - [#1159](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1159) -- Add more metrics about sessions in calls. - [#1183](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1183) - -### Changed -- dockerfile: create system user instead of normal user, avoid home directory - [#1058](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1058) -- Use "testing/synctest" to simplify timing-dependent tests. - [#1067](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1067) -- Add dedicated types for different session ids. - [#1066](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1066) -- Move "StringMap" class to api module. - [#1077](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1077) -- CI: Disable "stdversion" check of govet. - [#1079](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1079) -- CI: Use codecov components. - [#1080](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1080) -- Add interface for method "GetInCall". - [#1083](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1083) -- Make LruCache typed through generics. - [#1085](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1085) -- Protect access to the debug pprof handlers. - [#1094](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1094) -- Don't use environment to keep per-test properties. - [#1106](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1106) -- Add formatting to bandwidth values. - [#1114](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1114) -- Don't format zero bandwidth as "unlimited". - [#1115](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1115) -- CI: Split test jobs to speed up total actions time. - [#1118](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1118) -- Stop using global logger - [#1117](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1117) -- CI: Split tarball jobs to speed up total actions time. - [#1129](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1129) -- Update client code - [#1130](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1130) -- Don't use fmt.Sprintf where not necessary. - [#1131](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1131) -- Use test-related logger for embedded etcd. - [#1132](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1132) -- No need to use list of pointers, use objects directly. - [#1133](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1133) -- Generate shorter session ids. - [#1140](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1140) -- client: Include version, optimize JSON processing. - [#1143](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1143) -- Enable more linters - [#1145](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1145) -- Parallelize more tests. - [#1149](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1149) -- Move logging code to separate package. - [#1150](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1150) -- Close subscriber synchronously on errors. - [#1152](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1152) -- CI: Run "modernize" with Go 1.25 - [#1160](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1160) -- CI: Always use latest patch release for govuln checks. - [#1161](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1161) -- Process all NATS messages for same target from single goroutine. - [#1165](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1165) -- CI: Process files in all folders with licensecheck. - [#1169](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1169) -- CI: Run checklocks with Go 1.25 - [#1170](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1170) -- Refactor code into packages - [#1151](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1151) -- Remove unused testing code. - [#1174](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1174) -- checklocks: Remove ignore since generics are supported now. - [#1184](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1184) -- Support receiving and forwarding multiple chat messages from Talk. - [#1185](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1185) -- Move tests closer to code being checked - [#1186](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1186) - -### Fixed -- A proxy connection is only connected after a hello has been processed. - [#1071](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1071) -- Fix URL to send federated ping requests. - [#1081](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1081) -- Reconnect proxy connection even if shutdown was scheduled before. - [#1100](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1100) -- Federation cleanup fixes. - [#1105](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1105) -- Also rewrite token in comment for federated chat relay. - [#1112](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1112) -- Fix transient data for clustered setups. - [#1121](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1121) -- Fix initial transient data in clustered setups - [#1127](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1127) -- fix(docs): already_joined error response - [#1126](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1126) -- Fix storing initial data when clustered. - [#1128](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1128) -- Fix flaky tests that fail under load. - [#1153](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1153) - -### Dependencies -- Bump google.golang.org/grpc from 1.74.2 to 1.75.0 - [#1054](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1054) -- Bump github.com/nats-io/nats.go from 1.44.0 to 1.45.0 - [#1057](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1057) -- Bump google.golang.org/protobuf from 1.36.7 to 1.36.8 - [#1056](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1056) -- Bump github.com/stretchr/testify from 1.10.0 to 1.11.0 - [#1059](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1059) -- Bump github.com/stretchr/testify from 1.11.0 to 1.11.1 - [#1060](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1060) -- Bump markdown from 3.8.2 to 3.9 in /docs - [#1065](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1065) -- Bump github.com/pion/sdp/v3 from 3.0.15 to 3.0.16 - [#1061](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1061) -- Bump github.com/prometheus/client_golang from 1.23.0 to 1.23.2 - [#1064](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1064) -- Bump actions/setup-go from 5 to 6 - [#1062](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1062) -- Bump google.golang.org/grpc from 1.75.0 to 1.75.1 - [#1070](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1070) -- Bump google.golang.org/protobuf from 1.36.8 to 1.36.9 - [#1069](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1069) -- Bump github.com/nats-io/nats-server/v2 from 2.11.8 to 2.11.9 - [#1068](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1068) -- Bump github.com/mailru/easyjson from 0.9.0 to 0.9.1 - [#1072](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1072) -- Bump the etcd group with 4 updates - [#1073](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1073) -- Bump github.com/nats-io/nats.go from 1.45.0 to 1.46.0 - [#1076](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1076) -- Bump github.com/nats-io/nats-server/v2 from 2.11.9 to 2.12.0 - [#1075](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1075) -- Bump github.com/nats-io/nats.go from 1.46.0 to 1.46.1 - [#1087](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1087) -- Bump google.golang.org/grpc from 1.75.1 to 1.76.0 - [#1092](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1092) -- Bump github/codeql-action from 3 to 4 - [#1093](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1093) -- Bump peter-evans/create-or-update-comment from 4 to 5 - [#1090](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1090) -- Bump google.golang.org/protobuf from 1.36.9 to 1.36.10 - [#1089](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1089) -- Bump github.com/nats-io/nats.go from 1.46.1 to 1.47.0 - [#1096](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1096) -- Bump github.com/nats-io/nats-server/v2 from 2.12.0 to 2.12.1 - [#1095](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1095) -- Bump the artifacts group with 2 updates - [#1101](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1101) -- Bump markdown from 3.9 to 3.10 in /docs - [#1104](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1104) -- Bump golangci/golangci-lint-action from 8.0.0 to 9.0.0 - [#1110](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1110) -- Bump the etcd group with 4 updates - [#1111](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1111) -- Bump github.com/nats-io/nats-server/v2 from 2.12.1 to 2.12.2 - [#1113](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1113) -- Bump google.golang.org/grpc from 1.76.0 to 1.77.0 - [#1116](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1116) -- Bump golang.org/x/crypto from 0.43.0 to 0.45.0 - [#1119](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1119) -- Bump go.uber.org/zap from 1.27.0 to 1.27.1 - [#1123](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1123) -- Bump actions/checkout from 5 to 6 - [#1124](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1124) -- Bump golangci/golangci-lint-action from 9.0.0 to 9.1.0 - [#1125](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1125) -- Bump github.com/pion/ice/v4 from 4.0.10 to 4.0.11 - [#1135](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1135) -- Bump google.golang.org/grpc/cmd/protoc-gen-go-grpc from 1.5.1 to 1.6.0 - [#1136](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1136) -- Bump github.com/pion/ice/v4 from 4.0.11 to 4.0.12 - [#1138](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1138) -- Bump golangci/golangci-lint-action from 9.1.0 to 9.2.0 - [#1144](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1144) -- Bump github.com/pion/ice/v4 from 4.0.12 to 4.0.13 - [#1146](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1146) -- Bump actions/cache from 4 to 5 - [#1157](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1157) -- Bump google.golang.org/protobuf from 1.36.10 to 1.36.11 - [#1156](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1156) -- Bump github.com/pion/ice/v4 from 4.0.13 to 4.1.0 - [#1155](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1155) -- Bump the artifacts group with 2 updates - [#1154](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1154) -- Bump github.com/nats-io/nats.go from 1.47.0 to 1.48.0 - [#1163](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1163) -- Bump the etcd group with 4 updates - [#1162](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1162) -- Bump github.com/nats-io/nats-server/v2 from 2.12.2 to 2.12.3 - [#1164](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1164) -- Bump github.com/pion/sdp/v3 from 3.0.16 to 3.0.17 - [#1166](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1166) -- Bump google.golang.org/grpc from 1.77.0 to 1.78.0 - [#1167](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1167) -- Bump github.com/pion/ice/v4 from 4.1.0 to 4.2.0 - [#1171](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1171) -- Bump sphinx-rtd-theme from 3.0.2 to 3.1.0 in /docs - [#1172](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1172) -- Bump sphinx from 8.2.3 to 9.1.0 in /docs - [#1168](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1168) -- Bump markdown from 3.10 to 3.10.1 in /docs - [#1177](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1177) -- Bump github.com/nats-io/nats-server/v2 from 2.12.3 to 2.12.4 - [#1179](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1179) -- Bump github.com/golang-jwt/jwt/v5 from 5.3.0 to 5.3.1 - [#1182](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1182) - - -## 2.0.4 - 2025-08-18 - -### Added -- Comment / document possible error responses. - [#1004](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1004) -- Support multiple sessions for dialout. - [#1005](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1005) -- Support filtering candidates received by clients. - [#1000](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1000) -- Describe how to pass caller information for outgoing calls. - [#1019](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1019) -- Support multiple urls per backend - [#770](https://github.com/strukturag/nextcloud-spreed-signaling/pull/770) -- Return connection / publisher tokens for remote publishers. - [#1025](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1025) - -### Changed -- Drop support for Go 1.23 - [#1049](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1049) -- Only forward actor id / -type in "addsession" request if both are given. - [#1009](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1009) -- Use backend id in backend client stats to match other stats. - [#1020](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1020) -- Remove debug output. - [#1022](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1022) -- Only forward actor details in leave virtual sessions request if both are given. - [#1026](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1026) -- Delete (unused) proxy publisher/subscriber created after local timeout. - [#1032](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1032) -- modernize: Replace "interface{}" with "any". - [#1033](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1033) -- Add type for string maps. - [#1034](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1034) -- CI: Migrate to codecov. - [#1037](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1037) - [#1038](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1038) -- Use testify assertions to check expected fields / values internally. - [#1035](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1035) -- CI: Test with Golang 1.25 - [#1048](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1048) -- Modernize Go code and check from CI. - [#1050](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1050) -- Test "HasAnyPermission" method. - [#1051](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1051) -- Use standard library where possible. - [#1052](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1052) - -### Fixed -- Fix deadlock when setting transient data while removing listener. - [#992](https://github.com/strukturag/nextcloud-spreed-signaling/pull/992) -- Fixes for file watcher special cases - [#1017](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1017) -- Fix updating metric "signaling_mcu_subscribers" in various error cases. - [#1027](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1027) - -### Dependencies -- Bump google.golang.org/grpc from 1.72.0 to 1.72.1 - [#989](https://github.com/strukturag/nextcloud-spreed-signaling/pull/989) -- Bump github.com/pion/sdp/v3 from 3.0.11 to 3.0.12 - [#991](https://github.com/strukturag/nextcloud-spreed-signaling/pull/991) -- Bump the etcd group with 4 updates - [#990](https://github.com/strukturag/nextcloud-spreed-signaling/pull/990) -- Bump github.com/nats-io/nats-server/v2 from 2.11.3 to 2.11.4 - [#993](https://github.com/strukturag/nextcloud-spreed-signaling/pull/993) -- Bump github.com/pion/sdp/v3 from 3.0.12 to 3.0.13 - [#994](https://github.com/strukturag/nextcloud-spreed-signaling/pull/994) -- Bump google.golang.org/grpc from 1.72.1 to 1.72.2 - [#995](https://github.com/strukturag/nextcloud-spreed-signaling/pull/995) -- Bump github.com/nats-io/nats.go from 1.42.0 to 1.43.0 - [#999](https://github.com/strukturag/nextcloud-spreed-signaling/pull/999) -- Bump google.golang.org/grpc from 1.72.2 to 1.73.0 - [#1001](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1001) -- Bump the etcd group with 4 updates - [#1002](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1002) -- Bump markdown from 3.8 to 3.8.2 in /docs - [#1008](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1008) -- Bump github.com/pion/sdp/v3 from 3.0.13 to 3.0.14 - [#1006](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1006) -- Bump github.com/nats-io/nats-server/v2 from 2.11.4 to 2.11.5 - [#1010](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1010) -- Bump github.com/nats-io/nats-server/v2 from 2.11.5 to 2.11.6 - [#1011](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1011) -- Bump the etcd group with 4 updates - [#1015](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1015) -- Bump github.com/golang-jwt/jwt/v5 from 5.2.2 to 5.2.3 - [#1016](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1016) -- Bump google.golang.org/grpc from 1.73.0 to 1.74.0 - [#1018](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1018) -- Bump github.com/pion/sdp/v3 from 3.0.14 to 3.0.15 - [#1021](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1021) -- Bump the etcd group with 4 updates - [#1023](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1023) -- Bump google.golang.org/grpc from 1.74.0 to 1.74.2 - [#1024](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1024) -- Bump the etcd group with 4 updates - [#1028](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1028) -- Bump github.com/nats-io/nats.go from 1.43.0 to 1.44.0 - [#1031](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1031) -- Bump github.com/golang-jwt/jwt/v5 from 5.2.3 to 5.3.0 - [#1036](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1036) -- Bump github.com/prometheus/client_golang from 1.22.0 to 1.23.0 - [#1039](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1039) -- Bump github.com/nats-io/nats-server/v2 from 2.11.6 to 2.11.7 - [#1040](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1040) -- Bump google.golang.org/protobuf from 1.36.6 to 1.36.7 - [#1043](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1043) -- Bump actions/checkout from 4 to 5 - [#1044](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1044) -- Bump actions/download-artifact from 4 to 5 in the artifacts group - [#1042](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1042) -- Bump golang from 1.24-alpine to 1.25-alpine in /docker/server - [#1047](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1047) -- Bump golang from 1.24-alpine to 1.25-alpine in /docker/proxy - [#1046](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1046) -- Bump github.com/nats-io/nats-server/v2 from 2.11.7 to 2.11.8 - [#1053](https://github.com/strukturag/nextcloud-spreed-signaling/pull/1053) - - -## 2.0.3 - 2025-05-07 - -### Added -- Allow using environment variables for sessions and clients secrets - [#910](https://github.com/strukturag/nextcloud-spreed-signaling/pull/910) -- Allow using environment variables for backend secrets - [#912](https://github.com/strukturag/nextcloud-spreed-signaling/pull/912) -- Add serverinfo API - [#937](https://github.com/strukturag/nextcloud-spreed-signaling/pull/937) -- Add metrics for backend client requests. - [#973](https://github.com/strukturag/nextcloud-spreed-signaling/pull/973) - -### Changed -- Drop support for Go 1.22 - [#969](https://github.com/strukturag/nextcloud-spreed-signaling/pull/969) -- Do not log nats url credentials - [#911](https://github.com/strukturag/nextcloud-spreed-signaling/pull/911) -- Migrate cache-control parsing to https://github.com/pquerna/cachecontrol - [#916](https://github.com/strukturag/nextcloud-spreed-signaling/pull/916) -- CI: Test with Golang 1.24 - [#922](https://github.com/strukturag/nextcloud-spreed-signaling/pull/922) -- Add "/usr/lib64" to systemd ExecPath - [#963](https://github.com/strukturag/nextcloud-spreed-signaling/pull/963) -- Improve memory allocations - [#870](https://github.com/strukturag/nextcloud-spreed-signaling/pull/870) -- Speedup tests - [#972](https://github.com/strukturag/nextcloud-spreed-signaling/pull/972) -- docker: Make more settings configurable - [#980](https://github.com/strukturag/nextcloud-spreed-signaling/pull/980) -- Add jitter to reconnect intervals. - [#988](https://github.com/strukturag/nextcloud-spreed-signaling/pull/988) - -### Fixed -- nats: Reconnect client indefinitely. - [#935](https://github.com/strukturag/nextcloud-spreed-signaling/pull/935) -- Explicitly set TMPDIR to ensure that it is an executable path - [#956](https://github.com/strukturag/nextcloud-spreed-signaling/pull/956) -- Close subscribers on errors during initial connection. - [#959](https://github.com/strukturag/nextcloud-spreed-signaling/pull/959) -- Fix formatting of errors in "assert.Fail" calls. - [#970](https://github.com/strukturag/nextcloud-spreed-signaling/pull/970) -- Fix race condition in flaky certificate/CA reload tests. - [#971](https://github.com/strukturag/nextcloud-spreed-signaling/pull/971) -- Fix flaky test "Test_GrpcClients_DnsDiscovery". - [#976](https://github.com/strukturag/nextcloud-spreed-signaling/pull/976) -- Fix subscribers not closed when publisher is closed in Janus 1.x - [#986](https://github.com/strukturag/nextcloud-spreed-signaling/pull/986) -- Close subscriber if remote publisher was closed. - [#987](https://github.com/strukturag/nextcloud-spreed-signaling/pull/987) - -### Dependencies -- Bump google.golang.org/grpc from 1.69.4 to 1.70.0 - [#904](https://github.com/strukturag/nextcloud-spreed-signaling/pull/904) -- Bump the etcd group with 4 updates - [#907](https://github.com/strukturag/nextcloud-spreed-signaling/pull/907) -- Bump coverallsapp/github-action from 2.3.4 to 2.3.6 - [#909](https://github.com/strukturag/nextcloud-spreed-signaling/pull/909) -- Bump google.golang.org/protobuf from 1.36.3 to 1.36.4 - [#908](https://github.com/strukturag/nextcloud-spreed-signaling/pull/908) -- Bump github.com/nats-io/nats-server/v2 from 2.10.24 to 2.10.25 - [#903](https://github.com/strukturag/nextcloud-spreed-signaling/pull/903) -- Bump golangci/golangci-lint-action from 6.2.0 to 6.3.0 - [#913](https://github.com/strukturag/nextcloud-spreed-signaling/pull/913) -- Bump github.com/nats-io/nats.go from 1.38.0 to 1.39.0 - [#915](https://github.com/strukturag/nextcloud-spreed-signaling/pull/915) -- Bump golangci/golangci-lint-action from 6.3.0 to 6.3.2 - [#918](https://github.com/strukturag/nextcloud-spreed-signaling/pull/918) -- Bump google.golang.org/protobuf from 1.36.4 to 1.36.5 - [#917](https://github.com/strukturag/nextcloud-spreed-signaling/pull/917) -- build(deps): bump golang from 1.23-alpine to 1.24-alpine in /docker/proxy - [#921](https://github.com/strukturag/nextcloud-spreed-signaling/pull/921) -- build(deps): bump golang from 1.23-alpine to 1.24-alpine in /docker/server - [#919](https://github.com/strukturag/nextcloud-spreed-signaling/pull/919) -- build(deps): bump sphinx from 8.1.3 to 8.2.0 in /docs - [#928](https://github.com/strukturag/nextcloud-spreed-signaling/pull/928) -- build(deps): bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0 - [#927](https://github.com/strukturag/nextcloud-spreed-signaling/pull/927) -- build(deps): bump sphinx from 8.2.0 to 8.2.1 in /docs - [#929](https://github.com/strukturag/nextcloud-spreed-signaling/pull/929) -- build(deps): bump github.com/nats-io/nats.go from 1.39.0 to 1.39.1 - [#926](https://github.com/strukturag/nextcloud-spreed-signaling/pull/926) -- build(deps): bump sphinx from 8.2.1 to 8.2.3 in /docs - [#932](https://github.com/strukturag/nextcloud-spreed-signaling/pull/932) -- build(deps): bump google.golang.org/grpc from 1.70.0 to 1.71.0 - [#933](https://github.com/strukturag/nextcloud-spreed-signaling/pull/933) -- build(deps): bump golangci/golangci-lint-action from 6.3.3 to 6.5.0 - [#925](https://github.com/strukturag/nextcloud-spreed-signaling/pull/925) -- build(deps): bump jinja2 from 3.1.5 to 3.1.6 in /docs - [#938](https://github.com/strukturag/nextcloud-spreed-signaling/pull/938) -- build(deps): bump github.com/pion/sdp/v3 from 3.0.10 to 3.0.11 - [#939](https://github.com/strukturag/nextcloud-spreed-signaling/pull/939) -- build(deps): bump golangci/golangci-lint-action from 6.5.0 to 6.5.1 - [#940](https://github.com/strukturag/nextcloud-spreed-signaling/pull/940) -- build(deps): bump golangci/golangci-lint-action from 6.5.1 to 6.5.2 - [#946](https://github.com/strukturag/nextcloud-spreed-signaling/pull/946) -- build(deps): bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 - [#948](https://github.com/strukturag/nextcloud-spreed-signaling/pull/948) -- build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 - [#949](https://github.com/strukturag/nextcloud-spreed-signaling/pull/949) -- build(deps): bump golangci/golangci-lint-action from 6.5.2 to 7.0.0 - [#951](https://github.com/strukturag/nextcloud-spreed-signaling/pull/951) -- build(deps): bump google.golang.org/protobuf from 1.36.5 to 1.36.6 - [#952](https://github.com/strukturag/nextcloud-spreed-signaling/pull/952) -- Bump markdown from 3.7 to 3.8 in /docs - [#966](https://github.com/strukturag/nextcloud-spreed-signaling/pull/966) -- Bump golang.org/x/crypto from 0.32.0 to 0.35.0 - [#967](https://github.com/strukturag/nextcloud-spreed-signaling/pull/967) -- Bump github.com/nats-io/nats.go from 1.39.1 to 1.41.1 - [#964](https://github.com/strukturag/nextcloud-spreed-signaling/pull/964) -- Bump google.golang.org/grpc from 1.71.0 to 1.71.1 - [#957](https://github.com/strukturag/nextcloud-spreed-signaling/pull/957) -- build(deps): bump golang.org/x/net from 0.34.0 to 0.36.0 - [#941](https://github.com/strukturag/nextcloud-spreed-signaling/pull/941) -- build(deps): bump the etcd group with 4 updates - [#936](https://github.com/strukturag/nextcloud-spreed-signaling/pull/936) -- Bump github.com/nats-io/nats-server/v2 from 2.10.25 to 2.11.1 - [#962](https://github.com/strukturag/nextcloud-spreed-signaling/pull/962) -- Bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0 - [#974](https://github.com/strukturag/nextcloud-spreed-signaling/pull/974) -- Bump github.com/fsnotify/fsnotify from 1.8.0 to 1.9.0 - [#975](https://github.com/strukturag/nextcloud-spreed-signaling/pull/975) -- Bump google.golang.org/grpc from 1.71.1 to 1.72.0 - [#978](https://github.com/strukturag/nextcloud-spreed-signaling/pull/978) -- Bump github.com/nats-io/nats.go from 1.41.1 to 1.41.2 - [#977](https://github.com/strukturag/nextcloud-spreed-signaling/pull/977) -- Bump github.com/nats-io/nats-server/v2 from 2.11.1 to 2.11.3 - [#982](https://github.com/strukturag/nextcloud-spreed-signaling/pull/982) -- Bump github.com/nats-io/nats.go from 1.41.2 to 1.42.0 - [#983](https://github.com/strukturag/nextcloud-spreed-signaling/pull/983) -- Bump golangci/golangci-lint-action from 7.0.0 to 8.0.0 - [#985](https://github.com/strukturag/nextcloud-spreed-signaling/pull/985) - - ## 2.0.2 - 2025-01-22 ### Added diff --git a/Makefile b/Makefile index c2fbab3..3db88cf 100644 --- a/Makefile +++ b/Makefile @@ -6,28 +6,28 @@ GODIR := $(shell dirname "$(GO)") GOFMT := "$(GODIR)/gofmt" GOOS ?= linux GOARCH ?= amd64 -GOVERSION := $(shell "$(GO)" env GOVERSION | sed -E 's|go([0-9]+\.[0-9]+)\..*|\1|') -TMPDIR := $(CURDIR)/tmp +GOVERSION := $(shell "$(GO)" env GOVERSION | sed "s|go||" ) BINDIR := $(CURDIR)/bin VENDORDIR := "$(CURDIR)/vendor" VERSION := $(shell "$(CURDIR)/scripts/get-version.sh") TARVERSION := $(shell "$(CURDIR)/scripts/get-version.sh" --tar) PACKAGENAME := github.com/strukturag/nextcloud-spreed-signaling -GRPC_PROTO_FILES := $(basename $(wildcard grpc/*.proto)) +ALL_PACKAGES := $(PACKAGENAME) $(PACKAGENAME)/client $(PACKAGENAME)/proxy $(PACKAGENAME)/server +GRPC_PROTO_FILES := $(basename $(wildcard grpc_*.proto)) PROTOBUF_VERSION := $(shell grep google.golang.org/protobuf go.mod | xargs | cut -d ' ' -f 2) -PROTO_FILES := $(filter-out $(GRPC_PROTO_FILES),$(basename $(wildcard *.proto */*.proto))) +PROTO_FILES := $(filter-out $(GRPC_PROTO_FILES),$(basename $(wildcard *.proto))) PROTO_GO_FILES := $(addsuffix .pb.go,$(PROTO_FILES)) GRPC_PROTO_GO_FILES := $(addsuffix .pb.go,$(GRPC_PROTO_FILES)) $(addsuffix _grpc.pb.go,$(GRPC_PROTO_FILES)) -TEST_GO_FILES := $(wildcard *_test.go */*_test.go */*/*_test.go) -EASYJSON_FILES := $(filter-out $(TEST_GO_FILES),$(wildcard api*.go api/signaling.go */api.go */*/api.go talk/ocs.go)) +TEST_GO_FILES := $(wildcard *_test.go)) +EASYJSON_FILES := $(filter-out $(TEST_GO_FILES),$(wildcard api*.go)) EASYJSON_GO_FILES := $(patsubst %.go,%_easyjson.go,$(EASYJSON_FILES)) -COMMON_GO_FILES := $(filter-out geoip/continentmap.go $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES) $(EASYJSON_GO_FILES) $(TEST_GO_FILES),$(wildcard *.go */*.go */*/*.go)) -CLIENT_TEST_GO_FILES := $(wildcard cmd/client/*_test.go)) -CLIENT_GO_FILES := $(filter-out $(CLIENT_TEST_GO_FILES),$(wildcard cmd/client/*.go)) -SERVER_TEST_GO_FILES := $(wildcard cmd/server/*_test.go)) -SERVER_GO_FILES := $(filter-out $(SERVER_TEST_GO_FILES),$(wildcard cmd/server/*.go)) -PROXY_TEST_GO_FILES := $(wildcard cmd/proxy/*_test.go)) -PROXY_GO_FILES := $(filter-out $(PROXY_TEST_GO_FILES),$(wildcard cmd/proxy/*.go)) +COMMON_GO_FILES := $(filter-out continentmap.go $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES) $(EASYJSON_GO_FILES) $(TEST_GO_FILES),$(wildcard *.go)) +CLIENT_TEST_GO_FILES := $(wildcard client/*_test.go)) +CLIENT_GO_FILES := $(filter-out $(CLIENT_TEST_GO_FILES),$(wildcard client/*.go)) +SERVER_TEST_GO_FILES := $(wildcard server/*_test.go)) +SERVER_GO_FILES := $(filter-out $(SERVER_TEST_GO_FILES),$(wildcard server/*.go)) +PROXY_TEST_GO_FILES := $(wildcard proxy/*_test.go)) +PROXY_GO_FILES := $(filter-out $(PROXY_TEST_GO_FILES),$(wildcard proxy/*.go)) ifneq ($(VERSION),) INTERNALLDFLAGS := -X main.version=$(VERSION) @@ -51,10 +51,6 @@ ifeq ($(TIMEOUT),) TIMEOUT := 60s endif -ifeq ($(BENCHMARK),) -BENCHMARK := . -endif - ifneq ($(TEST),) TESTARGS := $(TESTARGS) -run "$(TEST)" endif @@ -77,12 +73,10 @@ else GOPATHBIN := $(GOPATH)/bin/$(GOOS)_$(GOARCH) endif -GOEXPERIMENT := - hook: [ ! -d "$(CURDIR)/.git/hooks" ] || ln -sf "$(CURDIR)/scripts/pre-commit.hook" "$(CURDIR)/.git/hooks/pre-commit" -$(GOPATHBIN)/easyjson: go.mod go.sum | $(TMPDIR) +$(GOPATHBIN)/easyjson: go.mod go.sum $(GO) install github.com/mailru/easyjson/... $(GOPATHBIN)/protoc-gen-go: go.mod go.sum @@ -91,10 +85,7 @@ $(GOPATHBIN)/protoc-gen-go: go.mod go.sum $(GOPATHBIN)/protoc-gen-go-grpc: go.mod go.sum $(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc -$(GOPATHBIN)/checklocks: go.mod go.sum - $(GO) install gvisor.dev/gvisor/tools/checklocks/cmd/checklocks@go - -geoip/continentmap.go: +continentmap.go: $(CURDIR)/scripts/get_continent_map.py $@ check-continentmap: @@ -102,41 +93,38 @@ check-continentmap: TMP=$$(mktemp -d) ;\ echo Make sure to remove $$TMP on error ;\ $(CURDIR)/scripts/get_continent_map.py $$TMP/continentmap.go ;\ - diff -u geoip/continentmap.go $$TMP/continentmap.go ;\ + diff -u continentmap.go $$TMP/continentmap.go ;\ rm -rf $$TMP get: $(GO) get $(PACKAGE) fmt: hook | $(PROTO_GO_FILES) - $(GOFMT) -s -w *.go cmd/client cmd/proxy cmd/server + $(GOFMT) -s -w *.go client proxy server vet: - GOEXPERIMENT=$(GOEXPERIMENT) $(GO) vet ./... + $(GO) vet $(ALL_PACKAGES) test: vet - GOEXPERIMENT=$(GOEXPERIMENT) $(GO) test -timeout $(TIMEOUT) $(TESTARGS) ./... - -benchmark: - GOEXPERIMENT=$(GOEXPERIMENT) $(GO) test -bench=$(BENCHMARK) -benchmem -run=^$$ -timeout $(TIMEOUT) $(TESTARGS) ./... - -checklocks: $(GOPATHBIN)/checklocks - GOEXPERIMENT=$(GOEXPERIMENT) $(GOPATHBIN)/checklocks ./... + $(GO) test -timeout $(TIMEOUT) $(TESTARGS) $(ALL_PACKAGES) cover: vet rm -f cover.out && \ - GOEXPERIMENT=$(GOEXPERIMENT) $(GO) test -timeout $(TIMEOUT) -coverprofile cover.out ./... + $(GO) test -timeout $(TIMEOUT) -coverprofile cover.out $(ALL_PACKAGES) && \ + sed -i "/_easyjson/d" cover.out && \ + sed -i "/\.pb\.go/d" cover.out && \ + $(GO) tool cover -func=cover.out coverhtml: vet rm -f cover.out && \ - GOEXPERIMENT=$(GOEXPERIMENT) $(GO) test -timeout $(TIMEOUT) -coverprofile cover.out ./... && \ + $(GO) test -timeout $(TIMEOUT) -coverprofile cover.out $(ALL_PACKAGES) && \ sed -i "/_easyjson/d" cover.out && \ sed -i "/\.pb\.go/d" cover.out && \ $(GO) tool cover -html=cover.out -o coverage.html %_easyjson.go: %.go $(GOPATHBIN)/easyjson | $(PROTO_GO_FILES) rm -f easyjson-bootstrap*.go - TMPDIR=$(TMPDIR) PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $*.go + PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $*.go %.pb.go: %.proto $(GOPATHBIN)/protoc-gen-go $(GOPATHBIN)/protoc-gen-go-grpc PATH="$(GODIR)":"$(GOPATHBIN)":$(PATH) protoc \ @@ -154,37 +142,32 @@ common: $(EASYJSON_GO_FILES) $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES) # Optimize easyjson files that could call generated functions instead of duplicating code. for file in $(EASYJSON_FILES); do \ rm -f easyjson-bootstrap*.go; \ - TMPDIR=$(TMPDIR) PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $$file; \ - rm -f *_easyjson_easyjson.go; \ + PATH="$(GODIR)":$(PATH) "$(GOPATHBIN)/easyjson" -all $$file; \ done $(BINDIR): mkdir -p "$(BINDIR)" -$(TMPDIR): - mkdir -p "$(TMPDIR)" - client: $(BINDIR)/client $(BINDIR)/client: go.mod go.sum $(CLIENT_GO_FILES) $(COMMON_GO_FILES) | $(BINDIR) - $(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./cmd/client/... + $(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./client/... server: $(BINDIR)/signaling $(BINDIR)/signaling: go.mod go.sum $(SERVER_GO_FILES) $(COMMON_GO_FILES) | $(BINDIR) - $(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./cmd/server/... + $(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./server/... proxy: $(BINDIR)/proxy $(BINDIR)/proxy: go.mod go.sum $(PROXY_GO_FILES) $(COMMON_GO_FILES) | $(BINDIR) - $(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./cmd/proxy/... + $(GO) build $(BUILDARGS) -ldflags '$(INTERNALLDFLAGS)' -o $@ ./proxy/... clean: rm -f easyjson-bootstrap*.go rm -f "$(BINDIR)/client" rm -f "$(BINDIR)/signaling" rm -f "$(BINDIR)/proxy" - rm -rf "$(TMPDIR)" clean-generated: clean rm -f $(EASYJSON_GO_FILES) $(PROTO_GO_FILES) $(GRPC_PROTO_GO_FILES) @@ -196,7 +179,7 @@ vendor: go.mod go.sum rm -rf $(VENDORDIR) $(GO) mod vendor -tarball: vendor | $(TMPDIR) +tarball: vendor git archive \ --prefix=nextcloud-spreed-signaling-$(TARVERSION)/ \ -o nextcloud-spreed-signaling-$(TARVERSION).tar \ @@ -206,17 +189,11 @@ tarball: vendor | $(TMPDIR) --mtime="$(shell git log -1 --date=iso8601-strict --format=%cd HEAD)" \ --transform "s//nextcloud-spreed-signaling-$(TARVERSION)\//" \ vendor - echo "$(TARVERSION)" > "$(TMPDIR)/version.txt" - tar rf nextcloud-spreed-signaling-$(TARVERSION).tar \ - -C "$(TMPDIR)" \ - --mtime="$(shell git log -1 --date=iso8601-strict --format=%cd HEAD)" \ - --transform "s//nextcloud-spreed-signaling-$(TARVERSION)\//" \ - version.txt gzip --force nextcloud-spreed-signaling-$(TARVERSION).tar dist: tarball .NOTPARALLEL: $(EASYJSON_GO_FILES) -.PHONY: geoip/continentmap.go common vendor +.PHONY: continentmap.go common vendor .SECONDARY: $(EASYJSON_GO_FILES) $(PROTO_GO_FILES) .DELETE_ON_ERROR: diff --git a/README.md b/README.md index ae20958..270aedf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Spreed standalone signaling server ![Build Status](https://github.com/strukturag/nextcloud-spreed-signaling/actions/workflows/test.yml/badge.svg) -[![Coverage Status](https://codecov.io/gh/strukturag/nextcloud-spreed-signaling/graph/badge.svg?token=IMXMIRNAJ8)](https://codecov.io/gh/strukturag/nextcloud-spreed-signaling) +[![Coverage Status](https://coveralls.io/repos/github/strukturag/nextcloud-spreed-signaling/badge.svg?branch=master)](https://coveralls.io/github/strukturag/nextcloud-spreed-signaling?branch=master) [![Documentation Status](https://readthedocs.org/projects/nextcloud-spreed-signaling/badge/?version=latest)](https://nextcloud-spreed-signaling.readthedocs.io/en/latest/?badge=latest) [![Go Report](https://goreportcard.com/badge/github.com/strukturag/nextcloud-spreed-signaling)](https://goreportcard.com/report/github.com/strukturag/nextcloud-spreed-signaling) @@ -17,7 +17,7 @@ information on the API of the signaling server. The following tools are required for building the signaling server. - git -- go >= 1.25 +- go >= 1.22 - make Usually the last two versions of Go are supported. This follows the release @@ -94,19 +94,11 @@ systemctl start signaling.service ### Running with Docker -Official docker images for the signaling server and -proxy are available on +Official docker containers for the signaling server and -proxy are available on Docker Hub at https://hub.docker.com/r/strukturag/nextcloud-spreed-signaling -See the `README.md` in the `docker` subfolder for details on how to use and -configure them. +See the `README.md` in the `docker` subfolder for details. -To build the images locally, run the following commands (replace the parameter -after `-t` with the name the image should be tagged as): - -```bash -docker build -f docker/server/Dockerfile -t nextcloud-spreed-signaling . -docker build -f docker/proxy/Dockerfile -t nextcloud-spreed-signaling-proxy . -``` #### Docker Compose @@ -139,30 +131,14 @@ server. A Janus server (from https://github.com/meetecho/janus-gateway) can be used to act as a WebRTC gateway. See the documentation of Janus on how to configure and -run the server. At least the `VideoRoom` plugin, the websocket transport and the -websocket events handler of Janus must be enabled. Also broadcasting of events -must be enabled. +run the server. At least the `VideoRoom` plugin and the websocket transport of +Janus must be enabled. The signaling server uses the `VideoRoom` plugin of Janus to manage sessions. All gateway details are hidden from the clients, all messages are sent through the signaling server. Only WebRTC media is exchanged directly between the gateway and the clients. -To enable sending of events from Janus, the option `broadcast` must be set to -`true` in the block `events` of `janus.jcfg`. In the configuration of the -websocket events handler (`janus.eventhandler.wsevh.jcfg`), the module must be -enabled by setting `enabled` to `true`, the `backend` must be set to the -websocket url of the signaling server (`ws://127.0.0.1:port/spreed`) or -proxy -(`ws://127.0.0.1:port/proxy`) and `subprotocol` must be set to `janus-events`. -At least events of type `handles`, `media` and `webrtc` must be subscribed. - -Warning: If the configuration between Janus and the signaling endpoint is -interrupted or can't be established, unsent events will be queued by Janus -and will use potentially lots of memory there. This can be limited by setting -`events_cap_on_reconnect` in `janus.eventhandler.wsevh.jcfg`. By default, all -events will be queued as the connection between Janus and the signaling endpoint -is assumed to be stable (most likely will be on the same machine). - Edit the `server.conf` and enter the URL to the websocket endpoint of Janus in the section `[mcu]` and key `url`. During startup, the signaling server will connect to Janus and log information of the gateway. diff --git a/container/ip_list.go b/allowed_ips.go similarity index 69% rename from container/ip_list.go rename to allowed_ips.go index ce6ed24..f401ec6 100644 --- a/container/ip_list.go +++ b/allowed_ips.go @@ -19,26 +19,23 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package container +package signaling import ( "bytes" "fmt" "net" - "slices" "strings" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" ) -type IPList struct { - ips []*net.IPNet +type AllowedIps struct { + allowed []*net.IPNet } -func (a *IPList) String() string { +func (a *AllowedIps) String() string { var b bytes.Buffer b.WriteString("[") - for idx, n := range a.ips { + for idx, n := range a.allowed { if idx > 0 { b.WriteString(", ") } @@ -48,14 +45,18 @@ func (a *IPList) String() string { return b.String() } -func (a *IPList) Empty() bool { - return len(a.ips) == 0 +func (a *AllowedIps) Empty() bool { + return len(a.allowed) == 0 } -func (a *IPList) Contains(ip net.IP) bool { - return slices.ContainsFunc(a.ips, func(n *net.IPNet) bool { - return n.Contains(ip) - }) +func (a *AllowedIps) Allowed(ip net.IP) bool { + for _, i := range a.allowed { + if i.Contains(ip) { + return true + } + } + + return false } func parseIPNet(s string) (*net.IPNet, error) { @@ -80,36 +81,35 @@ func parseIPNet(s string) (*net.IPNet, error) { return ipnet, nil } -func ParseIPList(allowed string) (*IPList, error) { +func ParseAllowedIps(allowed string) (*AllowedIps, error) { var allowedIps []*net.IPNet - for ip := range internal.SplitEntries(allowed, ",") { - i, err := parseIPNet(ip) - if err != nil { - return nil, err + for _, ip := range strings.Split(allowed, ",") { + ip = strings.TrimSpace(ip) + if ip != "" { + i, err := parseIPNet(ip) + if err != nil { + return nil, err + } + allowedIps = append(allowedIps, i) } - allowedIps = append(allowedIps, i) } - result := &IPList{ - ips: allowedIps, + result := &AllowedIps{ + allowed: allowedIps, } return result, nil } -func DefaultAllowedIPs() *IPList { +func DefaultAllowedIps() *AllowedIps { allowedIps := []*net.IPNet{ { IP: net.ParseIP("127.0.0.1"), Mask: net.CIDRMask(32, 32), }, - { - IP: net.ParseIP("::1"), - Mask: net.CIDRMask(128, 128), - }, } - result := &IPList{ - ips: allowedIps, + result := &AllowedIps{ + allowed: allowedIps, } return result } @@ -118,7 +118,6 @@ var ( privateIpNets = []string{ // Loopback addresses. "127.0.0.0/8", - "::1", // Private addresses. "10.0.0.0/8", "172.16.0.0/12", @@ -126,8 +125,8 @@ var ( } ) -func DefaultPrivateIPs() *IPList { - allowed, err := ParseIPList(strings.Join(privateIpNets, ",")) +func DefaultPrivateIps() *AllowedIps { + allowed, err := ParseAllowedIps(strings.Join(privateIpNets, ",")) if err != nil { panic(fmt.Errorf("could not parse private ips %+v: %w", privateIpNets, err)) } diff --git a/container/ip_list_test.go b/allowed_ips_test.go similarity index 56% rename from container/ip_list_test.go rename to allowed_ips_test.go index 73e6a2e..da4f49b 100644 --- a/container/ip_list_test.go +++ b/allowed_ips_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package container +package signaling import ( "net" @@ -29,67 +29,39 @@ import ( "github.com/stretchr/testify/require" ) -func TestIPList(t *testing.T) { - t.Parallel() +func TestAllowedIps(t *testing.T) { require := require.New(t) - a, err := ParseIPList("127.0.0.1, 192.168.0.1, 192.168.1.1/24") + a, err := ParseAllowedIps("127.0.0.1, 192.168.0.1, 192.168.1.1/24") require.NoError(err) require.False(a.Empty()) require.Equal(`[127.0.0.1/32, 192.168.0.1/32, 192.168.1.0/24]`, a.String()) - contained := []string{ + allowed := []string{ "127.0.0.1", "192.168.0.1", "192.168.1.1", "192.168.1.100", } - notContained := []string{ + notAllowed := []string{ "192.168.0.2", "10.1.2.3", } - for _, addr := range contained { + for _, addr := range allowed { t.Run(addr, func(t *testing.T) { - t.Parallel() assert := assert.New(t) if ip := net.ParseIP(addr); assert.NotNil(ip, "error parsing %s", addr) { - assert.True(a.Contains(ip), "should contain %s", addr) + assert.True(a.Allowed(ip), "should allow %s", addr) } }) } - for _, addr := range notContained { + for _, addr := range notAllowed { t.Run(addr, func(t *testing.T) { - t.Parallel() assert := assert.New(t) if ip := net.ParseIP(addr); assert.NotNil(ip, "error parsing %s", addr) { - assert.False(a.Contains(ip), "should not contain %s", addr) + assert.False(a.Allowed(ip), "should not allow %s", addr) } }) } } - -func TestDefaultAllowedIPs(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - ips := DefaultAllowedIPs() - assert.True(ips.Contains(net.ParseIP("127.0.0.1"))) - assert.False(ips.Contains(net.ParseIP("127.1.0.1"))) - assert.True(ips.Contains(net.ParseIP("::1"))) - assert.False(ips.Contains(net.ParseIP("1.1.1.1"))) -} - -func TestDefaultPrivateIPs(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - ips := DefaultPrivateIPs() - assert.True(ips.Contains(net.ParseIP("127.0.0.1"))) - assert.True(ips.Contains(net.ParseIP("127.1.0.1"))) - assert.True(ips.Contains(net.ParseIP("::1"))) - assert.True(ips.Contains(net.ParseIP("10.1.2.3"))) - assert.True(ips.Contains(net.ParseIP("172.16.17.18"))) - assert.True(ips.Contains(net.ParseIP("192.168.10.20"))) - assert.False(ips.Contains(net.ParseIP("1.1.1.1"))) -} diff --git a/api/bandwidth.go b/api/bandwidth.go deleted file mode 100644 index 52f66ae..0000000 --- a/api/bandwidth.go +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package api - -import ( - "fmt" - "math" - "sync/atomic" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" -) - -var ( - Kilobit = BandwidthFromBits(1024) - Megabit = BandwidthFromBits(1024) * Kilobit - Gigabit = BandwidthFromBits(1024) * Megabit -) - -// Bandwidth stores a bandwidth in bits per second. -type Bandwidth uint64 - -func formatWithRemainder(value uint64, divisor uint64, format string) string { - if value%divisor == 0 { - return fmt.Sprintf("%d %s", value/divisor, format) - } else { - v := float64(value) / float64(divisor) - v = math.Trunc(v*100) / 100 - return fmt.Sprintf("%.2f %s", v, format) - } -} - -// String returns the formatted bandwidth. -func (b Bandwidth) String() string { - switch { - case b >= Gigabit: - return formatWithRemainder(b.Bits(), Gigabit.Bits(), "Gbps") - case b >= Megabit: - return formatWithRemainder(b.Bits(), Megabit.Bits(), "Mbps") - case b >= Kilobit: - return formatWithRemainder(b.Bits(), Kilobit.Bits(), "Kbps") - default: - return fmt.Sprintf("%d bps", b) - } -} - -// Bits returns the bandwidth in bits per second. -func (b Bandwidth) Bits() uint64 { - return uint64(b) -} - -// Bytes returns the bandwidth in bytes per second. -func (b Bandwidth) Bytes() uint64 { - return b.Bits() / 8 -} - -// BandwidthFromBits creates a bandwidth from bits per second. -func BandwidthFromBits(b uint64) Bandwidth { - return Bandwidth(b) -} - -// BandwithFromBits creates a bandwidth from megabits per second. -func BandwidthFromMegabits(b uint64) Bandwidth { - return Bandwidth(b) * Megabit -} - -// BandwidthFromBytes creates a bandwidth from bytes per second. -func BandwidthFromBytes(b uint64) Bandwidth { - return Bandwidth(b * 8) -} - -// AtomicBandwidth is an atomic Bandwidth. The zero value is zero. -// AtomicBandwidth must not be copied after first use. -type AtomicBandwidth struct { - // 64-bit members that are accessed atomically must be 64-bit aligned. - v uint64 - _ internal.NoCopy -} - -// Load atomically loads and returns the value stored in b. -func (b *AtomicBandwidth) Load() Bandwidth { - return Bandwidth(atomic.LoadUint64(&b.v)) // +checklocksignore -} - -// Store atomically stores v into b. -func (b *AtomicBandwidth) Store(v Bandwidth) { - atomic.StoreUint64(&b.v, uint64(v)) // +checklocksignore -} - -// Swap atomically stores v into b and returns the previous value. -func (b *AtomicBandwidth) Swap(v Bandwidth) Bandwidth { - return Bandwidth(atomic.SwapUint64(&b.v, uint64(v))) // +checklocksignore -} diff --git a/api/bandwidth_test.go b/api/bandwidth_test.go deleted file mode 100644 index 5cdb427..0000000 --- a/api/bandwidth_test.go +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package api - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBandwidth(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - - var b Bandwidth - assert.EqualValues(0, b.Bits()) - assert.EqualValues(0, b.Bytes()) - - b = BandwidthFromBits(8000) - assert.EqualValues(8000, b.Bits()) - assert.EqualValues(1000, b.Bytes()) - - b = BandwidthFromBytes(1000) - assert.EqualValues(8000, b.Bits()) - assert.EqualValues(1000, b.Bytes()) - - b = BandwidthFromMegabits(2) - assert.EqualValues(2*1024*1024, b.Bits()) - assert.EqualValues(2*1024*1024/8, b.Bytes()) - - var a AtomicBandwidth - assert.EqualValues(0, a.Load()) - a.Store(1000) - assert.EqualValues(1000, a.Load()) - old := a.Swap(2000) - assert.EqualValues(1000, old) - assert.EqualValues(2000, a.Load()) -} - -func TestBandwidthString(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - testcases := []struct { - value Bandwidth - expected string - }{ - { - 0, - "0 bps", - }, - { - BandwidthFromBits(123), - "123 bps", - }, - { - BandwidthFromBits(1023), - "1023 bps", - }, - { - BandwidthFromBits(1024), - "1 Kbps", - }, - { - BandwidthFromBits(1024 + 512), - "1.50 Kbps", - }, - { - BandwidthFromBits(1024*1024 - 1), - "1023.99 Kbps", - }, - { - BandwidthFromBits(1024 * 1024), - "1 Mbps", - }, - { - BandwidthFromBits(1024*1024*1024 - 1), - "1023.99 Mbps", - }, - { - BandwidthFromBits(1024 * 1024 * 1024), - "1 Gbps", - }, - } - - for idx, tc := range testcases { - assert.Equal(tc.expected, tc.value.String(), "failed for testcase %d (%d)", idx, tc.value.Bits()) - } -} diff --git a/api/signaling_test.go b/api/signaling_test.go deleted file mode 100644 index b03faf4..0000000 --- a/api/signaling_test.go +++ /dev/null @@ -1,1081 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package api - -import ( - "encoding/json" - "errors" - "slices" - "strings" - "testing" - - "github.com/pion/ice/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" -) - -func TestRoomSessionIds(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - - var s1 RoomSessionId = "foo" - assert.False(s1.IsFederated()) - assert.EqualValues("foo", s1.WithoutFederation()) - - var s2 RoomSessionId = "federated|bar" - assert.True(s2.IsFederated()) - assert.EqualValues("bar", s2.WithoutFederation()) -} - -type testCheckValid interface { - CheckValid() error -} - -func wrapMessage(messageType string, msg testCheckValid) *ClientMessage { - wrapped := &ClientMessage{ - Type: messageType, - } - switch messageType { - case "hello": - wrapped.Hello = msg.(*HelloClientMessage) - case "message": - wrapped.Message = msg.(*MessageClientMessage) - case "bye": - wrapped.Bye = msg.(*ByeClientMessage) - case "room": - wrapped.Room = msg.(*RoomClientMessage) - case "control": - wrapped.Control = msg.(*ControlClientMessage) - case "internal": - wrapped.Internal = msg.(*InternalClientMessage) - case "transient": - wrapped.TransientData = msg.(*TransientDataClientMessage) - default: - return nil - } - return wrapped -} - -func testMessages(t *testing.T, messageType string, valid_messages []testCheckValid, invalid_messages []testCheckValid) { - t.Helper() - assert := assert.New(t) - for _, msg := range valid_messages { - assert.NoError(msg.CheckValid(), "Message %+v should be valid", msg) - - if messageType != "" { - // If the inner message is valid, it should also be valid in a wrapped - // ClientMessage. - if wrapped := wrapMessage(messageType, msg); assert.NotNil(wrapped, "Unknown message type: %s", messageType) { - assert.NoError(wrapped.CheckValid(), "Message %+v should be valid", wrapped) - } - } - } - for _, msg := range invalid_messages { - assert.Error(msg.CheckValid(), "Message %+v should not be valid", msg) - - if messageType != "" { - // If the inner message is invalid, it should also be invalid in a - // wrapped ClientMessage. - if wrapped := wrapMessage(messageType, msg); assert.NotNil(wrapped, "Unknown message type: %s", messageType) { - assert.Error(wrapped.CheckValid(), "Message %+v should not be valid", wrapped) - } - } - } -} - -func TestClientMessage(t *testing.T) { - t.Parallel() - assert := assert.New(t) - // The message needs a type. - msg := ClientMessage{} - assert.Error(msg.CheckValid()) -} - -func TestHelloClientMessage(t *testing.T) { - t.Parallel() - internalAuthParams := []byte("{\"backend\":\"https://domain.invalid\"}") - tokenAuthParams := []byte("{\"token\":\"invalid-token\"}") - valid_messages := []testCheckValid{ - // Hello version 1 - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Params: json.RawMessage("{}"), - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Type: "client", - Params: json.RawMessage("{}"), - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Type: "internal", - Params: internalAuthParams, - }, - }, - &HelloClientMessage{ - Version: HelloVersionV1, - ResumeId: "the-resume-id", - }, - // Hello version 2 - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Params: tokenAuthParams, - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Type: "client", - Params: tokenAuthParams, - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - ResumeId: "the-resume-id", - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Type: "federation", - Params: tokenAuthParams, - Url: "https://domain.invalid", - }, - }, - } - invalid_messages := []testCheckValid{ - // Hello version 1 - &HelloClientMessage{}, - &HelloClientMessage{Version: "0.0"}, - &HelloClientMessage{Version: HelloVersionV1}, - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Params: json.RawMessage("{}"), - Type: "invalid-type", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Params: json.RawMessage("{}"), - }, - }, - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Params: json.RawMessage("{}"), - Url: "invalid-url", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Type: "internal", - Params: json.RawMessage("{}"), - }, - }, - &HelloClientMessage{ - Version: HelloVersionV1, - Auth: &HelloClientMessageAuth{ - Type: "internal", - Params: json.RawMessage("xyz"), // Invalid JSON. - }, - }, - // Hello version 2 - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Params: tokenAuthParams, - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Params: tokenAuthParams, - Url: "invalid-url", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Params: internalAuthParams, - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Params: json.RawMessage("xyz"), // Invalid JSON. - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Type: HelloClientTypeFederation, - Params: json.RawMessage("xyz"), // Invalid JSON. - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Type: HelloClientTypeFederation, - Params: json.RawMessage("{}"), - Url: "https://domain.invalid", - }, - }, - &HelloClientMessage{ - Version: HelloVersionV2, - Auth: &HelloClientMessageAuth{ - Type: HelloClientTypeFederation, - Params: tokenAuthParams, - }, - }, - } - - testMessages(t, "hello", valid_messages, invalid_messages) - - // A "hello" message must be present - msg := ClientMessage{ - Type: "hello", - } - assert := assert.New(t) - assert.Error(msg.CheckValid()) -} - -func TestMessageClientMessage(t *testing.T) { - t.Parallel() - valid_messages := []testCheckValid{ - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "session", - SessionId: "the-session-id", - }, - Data: json.RawMessage("{}"), - }, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "user", - UserId: "the-user-id", - }, - Data: json.RawMessage("{}"), - }, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "room", - }, - Data: json.RawMessage("{}"), - }, - } - invalid_messages := []testCheckValid{ - &MessageClientMessage{}, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "session", - SessionId: "the-session-id", - }, - }, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "session", - }, - Data: json.RawMessage("{}"), - }, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "session", - UserId: "the-user-id", - }, - Data: json.RawMessage("{}"), - }, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "user", - }, - Data: json.RawMessage("{}"), - }, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "user", - UserId: "the-user-id", - }, - }, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "user", - SessionId: "the-user-id", - }, - Data: json.RawMessage("{}"), - }, - &MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "unknown-type", - }, - Data: json.RawMessage("{}"), - }, - } - testMessages(t, "message", valid_messages, invalid_messages) - - // A "message" message must be present - msg := ClientMessage{ - Type: "message", - } - assert := assert.New(t) - assert.Error(msg.CheckValid()) -} - -func TestControlClientMessage(t *testing.T) { - t.Parallel() - valid_messages := []testCheckValid{ - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "session", - SessionId: "the-session-id", - }, - Data: json.RawMessage("{}"), - }, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "user", - UserId: "the-user-id", - }, - Data: json.RawMessage("{}"), - }, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "room", - }, - Data: json.RawMessage("{}"), - }, - }, - } - invalid_messages := []testCheckValid{ - &ControlClientMessage{ - MessageClientMessage{}, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "session", - SessionId: "the-session-id", - }, - }, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "session", - }, - Data: json.RawMessage("{}"), - }, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "session", - UserId: "the-user-id", - }, - Data: json.RawMessage("{}"), - }, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "user", - }, - Data: json.RawMessage("{}"), - }, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "user", - UserId: "the-user-id", - }, - }, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "user", - SessionId: "the-user-id", - }, - Data: json.RawMessage("{}"), - }, - }, - &ControlClientMessage{ - MessageClientMessage{ - Recipient: MessageClientMessageRecipient{ - Type: "unknown-type", - }, - Data: json.RawMessage("{}"), - }, - }, - } - testMessages(t, "control", valid_messages, invalid_messages) - - // But a "control" message must be present - msg := ClientMessage{ - Type: "control", - } - assert := assert.New(t) - assert.Error(msg.CheckValid()) -} - -func TestMessageClientMessageData(t *testing.T) { - t.Parallel() - valid_messages := []testCheckValid{ - &MessageClientMessageData{ - Type: "invalid", - RoomType: "video", - }, - &MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - }, - &MessageClientMessageData{ - Type: "answer", - RoomType: "video", - Payload: StringMap{ - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, - &MessageClientMessageData{ - Type: "candidate", - RoomType: "video", - Payload: StringMap{ - "candidate": StringMap{ - "candidate": "", - }, - }, - }, - &MessageClientMessageData{ - Type: "candidate", - RoomType: "video", - Payload: StringMap{ - "candidate": StringMap{ - "candidate": "candidate:0 1 UDP 2122194687 192.0.2.4 61665 typ host", - }, - }, - }, - } - invalid_messages := []testCheckValid{ - &MessageClientMessageData{}, - &MessageClientMessageData{ - RoomType: "invalid", - }, - &MessageClientMessageData{ - Type: "offer", - RoomType: "video", - }, - &MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: StringMap{ - "sdp": 1234, - }, - }, - &MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: StringMap{ - "sdp": "invalid-sdp", - }, - }, - &MessageClientMessageData{ - Type: "answer", - RoomType: "video", - }, - &MessageClientMessageData{ - Type: "answer", - RoomType: "video", - Payload: StringMap{ - "sdp": 1234, - }, - }, - &MessageClientMessageData{ - Type: "answer", - RoomType: "video", - Payload: StringMap{ - "sdp": "invalid-sdp", - }, - }, - &MessageClientMessageData{ - Type: "candidate", - RoomType: "video", - }, - &MessageClientMessageData{ - Type: "candidate", - RoomType: "video", - Payload: StringMap{ - "candidate": "invalid-candidate", - }, - }, - &MessageClientMessageData{ - Type: "candidate", - RoomType: "video", - Payload: StringMap{ - "candidate": StringMap{ - "candidate": 12345, - }, - }, - }, - &MessageClientMessageData{ - Type: "candidate", - RoomType: "video", - Payload: StringMap{ - "candidate": StringMap{ - "candidate": ":", - }, - }, - }, - } - - testMessages(t, "", valid_messages, invalid_messages) -} - -func TestByeClientMessage(t *testing.T) { - t.Parallel() - // Any "bye" message is valid. - valid_messages := []testCheckValid{ - &ByeClientMessage{}, - } - invalid_messages := []testCheckValid{} - - testMessages(t, "bye", valid_messages, invalid_messages) - - // The "bye" message is optional. - msg := ClientMessage{ - Type: "bye", - } - assert := assert.New(t) - assert.NoError(msg.CheckValid()) -} - -func TestRoomClientMessage(t *testing.T) { - t.Parallel() - // Any regular "room" message is valid. - valid_messages := []testCheckValid{ - &RoomClientMessage{}, - &RoomClientMessage{ - Federation: &RoomFederationMessage{ - SignalingUrl: "http://signaling.domain.invalid/", - NextcloudUrl: "http://nextcloud.domain.invalid", - Token: "the token", - }, - }, - } - invalid_messages := []testCheckValid{ - &RoomClientMessage{ - Federation: &RoomFederationMessage{}, - }, - &RoomClientMessage{ - Federation: &RoomFederationMessage{ - SignalingUrl: ":", - }, - }, - &RoomClientMessage{ - Federation: &RoomFederationMessage{ - SignalingUrl: "http://signaling.domain.invalid", - }, - }, - &RoomClientMessage{ - Federation: &RoomFederationMessage{ - SignalingUrl: "http://signaling.domain.invalid/", - NextcloudUrl: ":", - }, - }, - &RoomClientMessage{ - Federation: &RoomFederationMessage{ - SignalingUrl: "http://signaling.domain.invalid/", - NextcloudUrl: "http://nextcloud.domain.invalid", - }, - }, - } - - testMessages(t, "room", valid_messages, invalid_messages) - - // But a "room" message must be present - msg := ClientMessage{ - Type: "room", - } - assert := assert.New(t) - assert.Error(msg.CheckValid()) -} - -func TestInternalClientMessage(t *testing.T) { - t.Parallel() - valid_messages := []testCheckValid{ - &InternalClientMessage{ - Type: "invalid", - }, - &InternalClientMessage{ - Type: "addsession", - AddSession: &AddSessionInternalClientMessage{ - CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ - SessionId: "session id", - RoomId: "room id", - }, - }, - }, - &InternalClientMessage{ - Type: "updatesession", - UpdateSession: &UpdateSessionInternalClientMessage{ - CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ - SessionId: "session id", - RoomId: "room id", - }, - }, - }, - &InternalClientMessage{ - Type: "removesession", - RemoveSession: &RemoveSessionInternalClientMessage{ - CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ - SessionId: "session id", - RoomId: "room id", - }, - }, - }, - &InternalClientMessage{ - Type: "incall", - InCall: &InCallInternalClientMessage{}, - }, - &InternalClientMessage{ - Type: "dialout", - Dialout: &DialoutInternalClientMessage{ - Type: "invalid", - }, - }, - &InternalClientMessage{ - Type: "dialout", - Dialout: &DialoutInternalClientMessage{ - Type: "error", - Error: &Error{}, - }, - }, - &InternalClientMessage{ - Type: "dialout", - Dialout: &DialoutInternalClientMessage{ - Type: "status", - Status: &DialoutStatusInternalClientMessage{}, - }, - }, - } - invalid_messages := []testCheckValid{ - &InternalClientMessage{}, - &InternalClientMessage{ - Type: "addsession", - }, - &InternalClientMessage{ - Type: "addsession", - AddSession: &AddSessionInternalClientMessage{}, - }, - &InternalClientMessage{ - Type: "addsession", - AddSession: &AddSessionInternalClientMessage{ - CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ - SessionId: "session id", - }, - }, - }, - &InternalClientMessage{ - Type: "updatesession", - }, - &InternalClientMessage{ - Type: "updatesession", - UpdateSession: &UpdateSessionInternalClientMessage{}, - }, - &InternalClientMessage{ - Type: "updatesession", - UpdateSession: &UpdateSessionInternalClientMessage{ - CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ - SessionId: "session id", - }, - }, - }, - &InternalClientMessage{ - Type: "removesession", - }, - &InternalClientMessage{ - Type: "removesession", - RemoveSession: &RemoveSessionInternalClientMessage{}, - }, - &InternalClientMessage{ - Type: "removesession", - RemoveSession: &RemoveSessionInternalClientMessage{ - CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ - SessionId: "session id", - }, - }, - }, - &InternalClientMessage{ - Type: "incall", - }, - &InternalClientMessage{ - Type: "dialout", - }, - &InternalClientMessage{ - Type: "dialout", - Dialout: &DialoutInternalClientMessage{}, - }, - &InternalClientMessage{ - Type: "dialout", - Dialout: &DialoutInternalClientMessage{ - Type: "error", - }, - }, - &InternalClientMessage{ - Type: "dialout", - Dialout: &DialoutInternalClientMessage{ - Type: "status", - }, - }, - } - - testMessages(t, "internal", valid_messages, invalid_messages) - - // But a "internal" message must be present - msg := ClientMessage{ - Type: "internal", - } - assert := assert.New(t) - assert.Error(msg.CheckValid()) -} - -func TestTransientDataClientMessage(t *testing.T) { - t.Parallel() - valid_messages := []testCheckValid{ - &TransientDataClientMessage{ - Type: "set", - Key: "foo", - }, - &TransientDataClientMessage{ - Type: "remove", - Key: "foo", - }, - } - invalid_messages := []testCheckValid{ - &TransientDataClientMessage{}, - &TransientDataClientMessage{ - Type: "set", - }, - &TransientDataClientMessage{ - Type: "remove", - }, - } - - testMessages(t, "transient", valid_messages, invalid_messages) - - // But a "transient" message must be present - msg := ClientMessage{ - Type: "transient", - } - assert := assert.New(t) - assert.Error(msg.CheckValid()) -} - -func TestErrorMessages(t *testing.T) { - t.Parallel() - assert := assert.New(t) - id := "request-id" - msg := ClientMessage{ - Id: id, - } - err1 := msg.NewErrorServerMessage(&Error{}) - assert.Equal(id, err1.Id, "%+v", err1) - assert.Equal("error", err1.Type, "%+v", err1) - assert.NotNil(err1.Error, "%+v", err1) - - err2 := msg.NewWrappedErrorServerMessage(errors.New("test-error")) - assert.Equal(id, err2.Id, "%+v", err2) - assert.Equal("error", err2.Type, "%+v", err2) - if assert.NotNil(err2.Error, "%+v", err2) { - assert.Equal("internal_error", err2.Error.Code, "%+v", err2) - assert.Equal("test-error", err2.Error.Message, "%+v", err2) - } - // Test "error" interface - assert.Equal("test-error", err2.Error.Error(), "%+v", err2) -} - -func TestIsChatRefresh(t *testing.T) { - t.Parallel() - var msg ServerMessage - data_true := []byte("{\"type\":\"chat\",\"chat\":{\"refresh\":true}}") - msg = ServerMessage{ - Type: "event", - Event: &EventServerMessage{ - Type: "message", - Message: &RoomEventMessage{ - RoomId: "foo", - Data: data_true, - }, - }, - } - assert.True(t, msg.IsChatRefresh()) - - data_false := []byte("{\"type\":\"chat\",\"chat\":{\"refresh\":false}}") - msg = ServerMessage{ - Type: "event", - Event: &EventServerMessage{ - Type: "message", - Message: &RoomEventMessage{ - RoomId: "foo", - Data: data_false, - }, - }, - } - assert.False(t, msg.IsChatRefresh()) -} - -func assertEqualStrings(t *testing.T, expected, result []string) { - t.Helper() - - if expected == nil { - expected = make([]string, 0) - } else { - slices.Sort(expected) - } - if result == nil { - result = make([]string, 0) - } else { - slices.Sort(result) - } - - assert.Equal(t, expected, result) -} - -func Test_Welcome_AddRemoveFeature(t *testing.T) { - t.Parallel() - assert := assert.New(t) - var msg WelcomeServerMessage - assertEqualStrings(t, []string{}, msg.Features) - - msg.AddFeature("one", "two", "one") - assertEqualStrings(t, []string{"one", "two"}, msg.Features) - assert.True(slices.IsSorted(msg.Features), "features should be sorted, got %+v", msg.Features) - - msg.AddFeature("three") - assertEqualStrings(t, []string{"one", "two", "three"}, msg.Features) - assert.True(slices.IsSorted(msg.Features), "features should be sorted, got %+v", msg.Features) - - msg.RemoveFeature("three", "one") - assertEqualStrings(t, []string{"two"}, msg.Features) -} - -func TestFilterCandidates(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - testcases := []struct { - candidate string - allowed string - blocked string - expectFiltered bool - }{ - // IPs can be filtered. - { - "candidate:1696121226 1 udp 2122129151 10.1.2.3 12345 typ host generation 0 ufrag YOs/ network-id 1", - "", - "10.0.0.0/8", - true, - }, - { - "candidate:1696121226 1 udp 2122129151 1.2.3.4 12345 typ host generation 0 ufrag YOs/ network-id 1", - "", - "10.0.0.0/8", - false, - }, - // IPs can be allowed. - { - "candidate:1696121226 1 udp 2122129151 10.1.2.3 12345 typ host generation 0 ufrag YOs/ network-id 1", - "10.1.0.0/16", - "10.0.0.0/8", - false, - }, - // IPs can be blocked. - { - "candidate:1696121226 1 udp 2122129151 1.2.3.4 12345 typ host generation 0 ufrag YOs/ network-id 1", - "", - "1.2.0.0/16", - true, - }, - } - - for idx, tc := range testcases { - candidate, err := ice.UnmarshalCandidate(tc.candidate) - if !assert.NoError(err, "parsing candidate %s failed in testcase %d", tc.candidate, idx) { - continue - } - - var allowed *container.IPList - if tc.allowed != "" { - allowed, err = container.ParseIPList(tc.allowed) - if !assert.NoError(err, "parsing allowed list %s failed in testcase %d", tc.allowed, idx) { - continue - } - } - - var blocked *container.IPList - if tc.blocked != "" { - blocked, err = container.ParseIPList(tc.blocked) - if !assert.NoError(err, "parsing blocked list %s failed in testcase %d", tc.blocked, idx) { - continue - } - } - - filtered := FilterCandidate(candidate, allowed, blocked) - assert.Equal(tc.expectFiltered, filtered, "failed in testcase %d", idx) - } -} - -func TestFilterSDPCandidates(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - s, err := ParseSDP(mock.MockSdpOfferAudioOnly) - require.NoError(err) - if encoded, err := s.Marshal(); assert.NoError(err) { - assert.Equal(mock.MockSdpOfferAudioOnly, strings.ReplaceAll(string(encoded), "\r\n", "\n")) - } - - expectedBefore := map[string]int{ - "audio": 4, - } - for _, m := range s.MediaDescriptions { - count := 0 - for _, a := range m.Attributes { - if a.IsICECandidate() { - count++ - } - } - - assert.Equal(expectedBefore[m.MediaName.Media], count, "invalid number of candidates for media description %s", m.MediaName.Media) - } - - blocked, err := container.ParseIPList("192.0.0.0/24, 192.168.0.0/16") - require.NoError(err) - - expectedAfter := map[string]int{ - "audio": 2, - } - if filtered := FilterSDPCandidates(s, nil, blocked); assert.True(filtered, "should have filtered") { - for _, m := range s.MediaDescriptions { - count := 0 - for _, a := range m.Attributes { - if a.IsICECandidate() { - count++ - } - } - - assert.Equal(expectedAfter[m.MediaName.Media], count, "invalid number of candidates for media description %s", m.MediaName.Media) - } - } - - if encoded, err := s.Marshal(); assert.NoError(err) { - assert.NotEqual(mock.MockSdpOfferAudioOnly, strings.ReplaceAll(string(encoded), "\r\n", "\n")) - assert.Equal(mock.MockSdpOfferAudioOnlyNoFilter, strings.ReplaceAll(string(encoded), "\r\n", "\n")) - } -} - -func TestNoFilterSDPCandidates(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - s, err := ParseSDP(mock.MockSdpOfferAudioOnlyNoFilter) - require.NoError(err) - if encoded, err := s.Marshal(); assert.NoError(err) { - assert.Equal(mock.MockSdpOfferAudioOnlyNoFilter, strings.ReplaceAll(string(encoded), "\r\n", "\n")) - } - - expectedBefore := map[string]int{ - "audio": 2, - } - for _, m := range s.MediaDescriptions { - count := 0 - for _, a := range m.Attributes { - if a.IsICECandidate() { - count++ - } - } - - assert.Equal(expectedBefore[m.MediaName.Media], count, "invalid number of candidates for media description %s", m.MediaName.Media) - } - - blocked, err := container.ParseIPList("192.0.0.0/24, 192.168.0.0/16") - require.NoError(err) - - expectedAfter := map[string]int{ - "audio": 2, - } - if filtered := FilterSDPCandidates(s, nil, blocked); assert.False(filtered, "should not have filtered") { - for _, m := range s.MediaDescriptions { - count := 0 - for _, a := range m.Attributes { - if a.IsICECandidate() { - count++ - } - } - - assert.Equal(expectedAfter[m.MediaName.Media], count, "invalid number of candidates for media description %s", m.MediaName.Media) - } - } - - if encoded, err := s.Marshal(); assert.NoError(err) { - assert.Equal(mock.MockSdpOfferAudioOnlyNoFilter, strings.ReplaceAll(string(encoded), "\r\n", "\n")) - } -} diff --git a/api/stringmap.go b/api/stringmap.go deleted file mode 100644 index de26597..0000000 --- a/api/stringmap.go +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package api - -// StringMap maps string keys to arbitrary values. -type StringMap map[string]any - -func (m StringMap) GetStringMap(key string) (StringMap, bool) { - v, found := m[key] - if !found { - return nil, false - } - - return ConvertStringMap(v) -} - -func ConvertStringMap(ob any) (StringMap, bool) { - if ob == nil { - return nil, true - } - - switch ob := ob.(type) { - case map[string]any: - return StringMap(ob), true - case StringMap: - return ob, true - default: - return nil, false - } -} - -// GetStringMapEntry returns an entry from a string map in a given type. -func GetStringMapEntry[T any](m StringMap, key string) (s T, ok bool) { - var defaultValue T - v, found := m[key] - if !found { - return defaultValue, false - } - - s, ok = v.(T) - return -} - -func GetStringMapString[T ~string](m StringMap, key string) (T, bool) { - var defaultValue T - v, found := m[key] - if !found { - return defaultValue, false - } - - switch v := v.(type) { - case string: - return T(v), true - case T: - return v, true - default: - return defaultValue, false - } -} diff --git a/api/stringmap_test.go b/api/stringmap_test.go deleted file mode 100644 index f54e3db..0000000 --- a/api/stringmap_test.go +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package api - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestConvertStringMap(t *testing.T) { - t.Parallel() - assert := assert.New(t) - d := map[string]any{ - "foo": "bar", - "bar": 2, - } - - m, ok := ConvertStringMap(d) - if assert.True(ok) { - assert.EqualValues(d, m) - } - - if m, ok := ConvertStringMap(nil); assert.True(ok) { - assert.Nil(m) - } - - _, ok = ConvertStringMap("foo") - assert.False(ok) - - _, ok = ConvertStringMap(1) - assert.False(ok) - - _, ok = ConvertStringMap(map[int]any{ - 1: "foo", - }) - assert.False(ok) -} - -func TestGetStringMapString(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - type StringMapTestString string - - var ok bool - m := StringMap{ - "foo": "bar", - "bar": StringMapTestString("baz"), - "baz": 1234, - } - if v, ok := GetStringMapString[string](m, "foo"); assert.True(ok) { - assert.Equal("bar", v) - } - if v, ok := GetStringMapString[StringMapTestString](m, "foo"); assert.True(ok) { - assert.Equal(StringMapTestString("bar"), v) - } - v, ok := GetStringMapString[string](m, "bar") - assert.False(ok, "should not find object, got %+v", v) - - if v, ok := GetStringMapString[StringMapTestString](m, "bar"); assert.True(ok) { - assert.Equal(StringMapTestString("baz"), v) - } - - _, ok = GetStringMapString[string](m, "baz") - assert.False(ok) - _, ok = GetStringMapString[StringMapTestString](m, "baz") - assert.False(ok) - _, ok = GetStringMapString[string](m, "invalid") - assert.False(ok) - _, ok = GetStringMapString[StringMapTestString](m, "invalid") - assert.False(ok) -} - -func TestGetStringMapStringMap(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - m := StringMap{ - "foo": map[string]any{ - "bar": 1, - }, - "bar": StringMap{ - "baz": 2, - }, - } - if v, ok := m.GetStringMap("foo"); assert.True(ok) { - assert.EqualValues(map[string]any{ - "bar": 1, - }, v) - } - if v, ok := m.GetStringMap("bar"); assert.True(ok) { - assert.EqualValues(map[string]any{ - "baz": 2, - }, v) - } - v, ok := m.GetStringMap("baz") - assert.False(ok, "expected missing entry, got %+v", v) -} diff --git a/api/transient_data.go b/api/transient_data.go deleted file mode 100644 index 6cc55c2..0000000 --- a/api/transient_data.go +++ /dev/null @@ -1,405 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package api - -import ( - "encoding/json" - "fmt" - "reflect" - "sync" - "time" -) - -const ( - TransientSessionDataPrefix = "sd:" -) - -type TransientListener interface { - SendMessage(message *ServerMessage) bool -} - -type TransientDataEntry struct { - Value any `json:"value"` - Expires time.Time `json:"expires,omitzero"` -} - -func NewTransientDataEntry(value any, ttl time.Duration) *TransientDataEntry { - entry := &TransientDataEntry{ - Value: value, - } - if ttl > 0 { - entry.Expires = time.Now().Add(ttl) - } - return entry -} - -func NewTransientDataEntryWithExpires(value any, expires time.Time) *TransientDataEntry { - entry := &TransientDataEntry{ - Value: value, - Expires: expires, - } - return entry -} - -func (e *TransientDataEntry) clone() *TransientDataEntry { - result := *e - return &result -} - -func (e *TransientDataEntry) update(value any, ttl time.Duration) { - e.Value = value - if ttl > 0 { - e.Expires = time.Now().Add(ttl) - } else { - e.Expires = time.Time{} - } -} - -type TransientDataEntries map[string]*TransientDataEntry - -func (e TransientDataEntries) String() string { - data, err := json.Marshal(e) - if err != nil { - return fmt.Sprintf("Could not serialize %#v: %s", e, err) - } - - return string(data) -} - -type TransientData struct { - mu sync.Mutex - // +checklocks:mu - data TransientDataEntries - // +checklocks:mu - listeners map[TransientListener]bool - // +checklocks:mu - timers map[string]*time.Timer -} - -// NewTransientData creates a new transient data container. -func NewTransientData() *TransientData { - return &TransientData{} -} - -// +checklocks:t.mu -func (t *TransientData) sendMessageToListener(listener TransientListener, message *ServerMessage) { - t.mu.Unlock() - defer t.mu.Lock() - - listener.SendMessage(message) -} - -// +checklocks:t.mu -func (t *TransientData) notifySet(key string, prev, value any) { - msg := &ServerMessage{ - Type: "transient", - TransientData: &TransientDataServerMessage{ - Type: "set", - Key: key, - Value: value, - OldValue: prev, - }, - } - for listener := range t.listeners { - t.sendMessageToListener(listener, msg) - } -} - -// +checklocks:t.mu -func (t *TransientData) notifyDeleted(key string, prev *TransientDataEntry) { - msg := &ServerMessage{ - Type: "transient", - TransientData: &TransientDataServerMessage{ - Type: "remove", - Key: key, - }, - } - if prev != nil { - msg.TransientData.OldValue = prev.Value - } - for listener := range t.listeners { - t.sendMessageToListener(listener, msg) - } -} - -// AddListener adds a new listener to be notified about changes. -func (t *TransientData) AddListener(listener TransientListener) { - t.mu.Lock() - defer t.mu.Unlock() - - if t.listeners == nil { - t.listeners = make(map[TransientListener]bool) - } - t.listeners[listener] = true - if len(t.data) > 0 { - data := make(StringMap, len(t.data)) - for k, v := range t.data { - data[k] = v.Value - } - msg := &ServerMessage{ - Type: "transient", - TransientData: &TransientDataServerMessage{ - Type: "initial", - Data: data, - }, - } - t.sendMessageToListener(listener, msg) - } -} - -// RemoveListener removes a previously registered listener. -func (t *TransientData) RemoveListener(listener TransientListener) { - t.mu.Lock() - defer t.mu.Unlock() - - delete(t.listeners, listener) -} - -// +checklocks:t.mu -func (t *TransientData) updateTTL(key string, value any, ttl time.Duration) { - if ttl <= 0 { - if old, found := t.timers[key]; found { - old.Stop() - delete(t.timers, key) - } - } else { - t.removeAfterTTL(key, value, ttl) - } -} - -// +checklocks:t.mu -func (t *TransientData) removeAfterTTL(key string, value any, ttl time.Duration) { - if old, found := t.timers[key]; found { - old.Stop() - } - - if ttl <= 0 { - delete(t.timers, key) - return - } - - timer := time.AfterFunc(ttl, func() { - t.mu.Lock() - defer t.mu.Unlock() - - t.compareAndRemove(key, value) - }) - if t.timers == nil { - t.timers = make(map[string]*time.Timer) - } - t.timers[key] = timer -} - -// +checklocks:t.mu -func (t *TransientData) doSet(key string, value any, prev *TransientDataEntry, ttl time.Duration) { - if t.data == nil { - t.data = make(TransientDataEntries) - } - var oldValue any - if prev == nil { - entry := NewTransientDataEntry(value, ttl) - t.data[key] = entry - } else { - oldValue = prev.Value - prev.update(value, ttl) - } - t.notifySet(key, oldValue, value) - t.removeAfterTTL(key, value, ttl) -} - -// Set sets a new value for the given key and notifies listeners -// if the value has been changed. -func (t *TransientData) Set(key string, value any) bool { - return t.SetTTL(key, value, 0) -} - -// SetTTL sets a new value for the given key with a time-to-live and notifies -// listeners if the value has been changed. -func (t *TransientData) SetTTL(key string, value any, ttl time.Duration) bool { - if value == nil { - return t.Remove(key) - } - - t.mu.Lock() - defer t.mu.Unlock() - - prev, found := t.data[key] - if found && reflect.DeepEqual(prev.Value, value) { - t.updateTTL(key, value, ttl) - return false - } - - t.doSet(key, value, prev, ttl) - return true -} - -// CompareAndSet sets a new value for the given key only for a given old value -// and notifies listeners if the value has been changed. -func (t *TransientData) CompareAndSet(key string, old, value any) bool { - return t.CompareAndSetTTL(key, old, value, 0) -} - -// CompareAndSetTTL sets a new value for the given key with a time-to-live, -// only for a given old value and notifies listeners if the value has been -// changed. -func (t *TransientData) CompareAndSetTTL(key string, old, value any, ttl time.Duration) bool { - if value == nil { - return t.CompareAndRemove(key, old) - } - - t.mu.Lock() - defer t.mu.Unlock() - - prev, found := t.data[key] - if old != nil && (!found || !reflect.DeepEqual(prev.Value, old)) { - return false - } else if old == nil && found { - return false - } - - t.doSet(key, value, prev, ttl) - return true -} - -// +checklocks:t.mu -func (t *TransientData) doRemove(key string, prev *TransientDataEntry) { - delete(t.data, key) - if old, found := t.timers[key]; found { - old.Stop() - delete(t.timers, key) - } - t.notifyDeleted(key, prev) -} - -// Remove deletes the value with the given key and notifies listeners -// if the key was removed. -func (t *TransientData) Remove(key string) bool { - t.mu.Lock() - defer t.mu.Unlock() - - prev, found := t.data[key] - if !found { - return false - } - - t.doRemove(key, prev) - return true -} - -// CompareAndRemove deletes the value with the given key if it has a given value -// and notifies listeners if the key was removed. -func (t *TransientData) CompareAndRemove(key string, old any) bool { - t.mu.Lock() - defer t.mu.Unlock() - - return t.compareAndRemove(key, old) -} - -// +checklocks:t.mu -func (t *TransientData) compareAndRemove(key string, old any) bool { - prev, found := t.data[key] - if !found || !reflect.DeepEqual(prev.Value, old) { - return false - } - - t.doRemove(key, prev) - return true -} - -// GetData returns a copy of the internal data. -func (t *TransientData) GetData() StringMap { - t.mu.Lock() - defer t.mu.Unlock() - - if len(t.data) == 0 { - return nil - } - - result := make(StringMap, len(t.data)) - for k, entry := range t.data { - result[k] = entry.Value - } - return result -} - -// GetEntries returns a copy of the internal data entries. -func (t *TransientData) GetEntries() TransientDataEntries { - t.mu.Lock() - defer t.mu.Unlock() - - if len(t.data) == 0 { - return nil - } - - result := make(TransientDataEntries, len(t.data)) - for k, e := range t.data { - result[k] = e.clone() - } - return result -} - -// SetInitial sets the initial data and notifies listeners. -func (t *TransientData) SetInitial(data TransientDataEntries) { - if len(data) == 0 { - return - } - - t.mu.Lock() - defer t.mu.Unlock() - - if t.data == nil { - t.data = make(TransientDataEntries) - } - - now := time.Now() - msgData := make(StringMap, len(data)) - for k, v := range data { - if _, found := t.data[k]; found { - // Entry already present (i.e. was set by regular event). - continue - } - - if e := v.Expires; !e.IsZero() { - if now.After(e) { - // Already expired - continue - } - - t.removeAfterTTL(k, v.Value, e.Sub(now)) - } - msgData[k] = v.Value - t.data[k] = v - } - if len(msgData) == 0 { - return - } - msg := &ServerMessage{ - Type: "transient", - TransientData: &TransientDataServerMessage{ - Type: "initial", - Data: msgData, - }, - } - for listener := range t.listeners { - t.sendMessageToListener(listener, msg) - } -} diff --git a/api/transient_data_test.go b/api/transient_data_test.go deleted file mode 100644 index ae3dd62..0000000 --- a/api/transient_data_test.go +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package api - -import ( - "sync" - "sync/atomic" - "testing" - "testing/synctest" - "time" - - "github.com/stretchr/testify/assert" -) - -func Test_TransientData(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - data := NewTransientData() - assert.False(data.Set("foo", nil)) - assert.True(data.Set("foo", "bar")) - assert.False(data.Set("foo", "bar")) - assert.True(data.Set("foo", "baz")) - assert.False(data.CompareAndSet("foo", "bar", "lala")) - assert.True(data.CompareAndSet("foo", "baz", "lala")) - assert.False(data.CompareAndSet("test", nil, nil)) - assert.True(data.CompareAndSet("test", nil, "123")) - assert.False(data.CompareAndSet("test", nil, "456")) - assert.False(data.CompareAndRemove("test", "1234")) - assert.True(data.CompareAndRemove("test", "123")) - assert.False(data.Remove("lala")) - assert.True(data.Remove("foo")) - - assert.True(data.SetTTL("test", "1234", time.Millisecond)) - assert.Equal("1234", data.GetData()["test"]) - // Data is removed after the TTL - start := time.Now() - time.Sleep(time.Millisecond) - synctest.Wait() - assert.Equal(time.Millisecond, time.Since(start)) - assert.Nil(data.GetData()["test"]) - - assert.True(data.SetTTL("test", "1234", time.Millisecond)) - assert.Equal("1234", data.GetData()["test"]) - assert.True(data.SetTTL("test", "2345", 3*time.Millisecond)) - assert.Equal("2345", data.GetData()["test"]) - start = time.Now() - // Data is removed after the TTL only if the value still matches - time.Sleep(2 * time.Millisecond) - synctest.Wait() - assert.Equal("2345", data.GetData()["test"]) - // Data is removed after the (second) TTL - time.Sleep(time.Millisecond) - synctest.Wait() - assert.Equal(3*time.Millisecond, time.Since(start)) - assert.Nil(data.GetData()["test"]) - - // Setting existing key will update the TTL - assert.True(data.SetTTL("test", "1234", time.Millisecond)) - assert.False(data.SetTTL("test", "1234", 3*time.Millisecond)) - start = time.Now() - // Data still exists after the first TTL - time.Sleep(2 * time.Millisecond) - synctest.Wait() - assert.Equal("1234", data.GetData()["test"]) - // Data is removed after the (updated) TTL - time.Sleep(time.Millisecond) - synctest.Wait() - assert.Equal(3*time.Millisecond, time.Since(start)) - assert.Nil(data.GetData()["test"]) - }) -} - -type MockTransientListener struct { - mu sync.Mutex - sending chan struct{} - done chan struct{} - - // +checklocks:mu - data *TransientData -} - -func (l *MockTransientListener) SendMessage(message *ServerMessage) bool { - close(l.sending) - - time.Sleep(10 * time.Millisecond) - - l.mu.Lock() - defer l.mu.Unlock() - defer close(l.done) - - time.Sleep(10 * time.Millisecond) - - return true -} - -func (l *MockTransientListener) Close() { - l.mu.Lock() - defer l.mu.Unlock() - - l.data.RemoveListener(l) -} - -func Test_TransientDataDeadlock(t *testing.T) { - t.Parallel() - data := NewTransientData() - - listener := &MockTransientListener{ - sending: make(chan struct{}), - done: make(chan struct{}), - - data: data, - } - data.AddListener(listener) - - go func() { - <-listener.sending - listener.Close() - }() - - data.Set("foo", "bar") - <-listener.done -} - -type initialDataListener struct { - t *testing.T - - expected StringMap - sent atomic.Int32 -} - -func (l *initialDataListener) SendMessage(message *ServerMessage) bool { - switch l.sent.Add(1) { - case 1: - if assert.Equal(l.t, "transient", message.Type) && - assert.NotNil(l.t, message.TransientData) && - assert.Equal(l.t, "initial", message.TransientData.Type) { - assert.Equal(l.t, l.expected, message.TransientData.Data) - } - case 2: - if assert.Equal(l.t, "transient", message.Type) && - assert.NotNil(l.t, message.TransientData) && - assert.Equal(l.t, "remove", message.TransientData.Type) { - assert.Equal(l.t, "foo", message.TransientData.Key) - assert.Equal(l.t, "bar", message.TransientData.OldValue) - } - default: - assert.Fail(l.t, "unexpected message", "received %+v", message) - } - return true -} - -func Test_TransientDataNotifyInitial(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - data := NewTransientData() - assert.True(data.Set("foo", "bar")) - - listener := &initialDataListener{ - t: t, - expected: StringMap{ - "foo": "bar", - }, - } - data.AddListener(listener) - assert.EqualValues(1, listener.sent.Load()) -} - -func Test_TransientDataSetInitial(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - - now := time.Now() - data := NewTransientData() - listener1 := &initialDataListener{ - t: t, - expected: StringMap{ - "foo": "bar", - "bar": 1234, - }, - } - data.AddListener(listener1) - assert.EqualValues(0, listener1.sent.Load()) - - data.SetInitial(TransientDataEntries{ - "foo": NewTransientDataEntryWithExpires("bar", now.Add(time.Minute)), - "bar": NewTransientDataEntry(1234, 0), - "expired": NewTransientDataEntryWithExpires(1234, now.Add(-time.Second)), - }) - - entries := data.GetEntries() - assert.Equal(TransientDataEntries{ - "foo": NewTransientDataEntryWithExpires("bar", now.Add(time.Minute)), - "bar": NewTransientDataEntry(1234, 0), - }, entries) - - listener2 := &initialDataListener{ - t: t, - expected: StringMap{ - "foo": "bar", - "bar": 1234, - }, - } - data.AddListener(listener2) - assert.EqualValues(1, listener1.sent.Load()) - assert.EqualValues(1, listener2.sent.Load()) - - time.Sleep(time.Minute) - synctest.Wait() - assert.EqualValues(2, listener1.sent.Load()) - assert.EqualValues(2, listener2.sent.Load()) - }) -} diff --git a/async/events/api.go b/api_async.go similarity index 69% rename from async/events/api.go rename to api_async.go index 26bb6ea..d3c0426 100644 --- a/async/events/api.go +++ b/api_async.go @@ -19,15 +19,12 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package events +package signaling import ( "encoding/json" "fmt" "time" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) type AsyncMessage struct { @@ -35,11 +32,11 @@ type AsyncMessage struct { Type string `json:"type"` - Message *api.ServerMessage `json:"message,omitempty"` + Message *ServerMessage `json:"message,omitempty"` - Room *talk.BackendServerRoomRequest `json:"room,omitempty"` + Room *BackendServerRoomRequest `json:"room,omitempty"` - Permissions []api.Permission `json:"permissions,omitempty"` + Permissions []Permission `json:"permissions,omitempty"` AsyncRoom *AsyncRoomMessage `json:"asyncroom,omitempty"` @@ -59,12 +56,12 @@ func (m *AsyncMessage) String() string { type AsyncRoomMessage struct { Type string `json:"type"` - SessionId api.PublicSessionId `json:"sessionid,omitempty"` - ClientType api.ClientType `json:"clienttype,omitempty"` + SessionId string `json:"sessionid,omitempty"` + ClientType string `json:"clienttype,omitempty"` } type SendOfferMessage struct { - MessageId string `json:"messageid,omitempty"` - SessionId api.PublicSessionId `json:"sessionid"` - Data *api.MessageClientMessageData `json:"data"` + MessageId string `json:"messageid,omitempty"` + SessionId string `json:"sessionid"` + Data *MessageClientMessageData `json:"data"` } diff --git a/async/events/api_easyjson.go b/api_async_easyjson.go similarity index 64% rename from async/events/api_easyjson.go rename to api_async_easyjson.go index 86cc164..1439e78 100644 --- a/async/events/api_easyjson.go +++ b/api_async_easyjson.go @@ -1,14 +1,12 @@ // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. -package events +package signaling import ( json "encoding/json" easyjson "github.com/mailru/easyjson" jlexer "github.com/mailru/easyjson/jlexer" jwriter "github.com/mailru/easyjson/jwriter" - api "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - talk "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) // suppress unused package warning @@ -19,7 +17,7 @@ var ( _ easyjson.Marshaler ) -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(in *jlexer.Lexer, out *SendOfferMessage) { +func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling(in *jlexer.Lexer, out *SendOfferMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -32,32 +30,25 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "messageid": - if in.IsNull() { - in.Skip() - } else { - out.MessageId = string(in.String()) - } + out.MessageId = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = api.PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "data": if in.IsNull() { in.Skip() out.Data = nil } else { if out.Data == nil { - out.Data = new(api.MessageClientMessageData) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Data).UnmarshalEasyJSON(in) + out.Data = new(MessageClientMessageData) } + (*out.Data).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -69,7 +60,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(out *jwriter.Writer, in SendOfferMessage) { +func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling(out *jwriter.Writer, in SendOfferMessage) { out.RawByte('{') first := true _ = first @@ -104,27 +95,27 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve // MarshalJSON supports json.Marshaler interface func (v SendOfferMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(&w, v) + easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v SendOfferMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(w, v) + easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *SendOfferMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(&r, v) + easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *SendOfferMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents(l, v) + easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(in *jlexer.Lexer, out *AsyncRoomMessage) { +func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling1(in *jlexer.Lexer, out *AsyncRoomMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -137,25 +128,18 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = api.PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "clienttype": - if in.IsNull() { - in.Skip() - } else { - out.ClientType = api.ClientType(in.String()) - } + out.ClientType = string(in.String()) default: in.SkipRecursive() } @@ -166,7 +150,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(out *jwriter.Writer, in AsyncRoomMessage) { +func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling1(out *jwriter.Writer, in AsyncRoomMessage) { out.RawByte('{') first := true _ = first @@ -191,27 +175,27 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve // MarshalJSON supports json.Marshaler interface func (v AsyncRoomMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(&w, v) + easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling1(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v AsyncRoomMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(w, v) + easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling1(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *AsyncRoomMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(&r, v) + easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling1(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AsyncRoomMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents1(l, v) + easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling1(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(in *jlexer.Lexer, out *AsyncMessage) { +func easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlexer.Lexer, out *AsyncMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -224,34 +208,27 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "sendtime": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.SendTime).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.SendTime).UnmarshalJSON(data)) } case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "message": if in.IsNull() { in.Skip() out.Message = nil } else { if out.Message == nil { - out.Message = new(api.ServerMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Message).UnmarshalEasyJSON(in) + out.Message = new(ServerMessage) } + (*out.Message).UnmarshalEasyJSON(in) } case "room": if in.IsNull() { @@ -259,13 +236,9 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve out.Room = nil } else { if out.Room == nil { - out.Room = new(talk.BackendServerRoomRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Room).UnmarshalEasyJSON(in) + out.Room = new(BackendServerRoomRequest) } + (*out.Room).UnmarshalEasyJSON(in) } case "permissions": if in.IsNull() { @@ -275,20 +248,16 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve in.Delim('[') if out.Permissions == nil { if !in.IsDelim(']') { - out.Permissions = make([]api.Permission, 0, 4) + out.Permissions = make([]Permission, 0, 4) } else { - out.Permissions = []api.Permission{} + out.Permissions = []Permission{} } } else { out.Permissions = (out.Permissions)[:0] } for !in.IsDelim(']') { - var v1 api.Permission - if in.IsNull() { - in.Skip() - } else { - v1 = api.Permission(in.String()) - } + var v1 Permission + v1 = Permission(in.String()) out.Permissions = append(out.Permissions, v1) in.WantComma() } @@ -302,11 +271,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve if out.AsyncRoom == nil { out.AsyncRoom = new(AsyncRoomMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.AsyncRoom).UnmarshalEasyJSON(in) - } + (*out.AsyncRoom).UnmarshalEasyJSON(in) } case "sendoffer": if in.IsNull() { @@ -316,18 +281,10 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve if out.SendOffer == nil { out.SendOffer = new(SendOfferMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.SendOffer).UnmarshalEasyJSON(in) - } + (*out.SendOffer).UnmarshalEasyJSON(in) } case "id": - if in.IsNull() { - in.Skip() - } else { - out.Id = string(in.String()) - } + out.Id = string(in.String()) default: in.SkipRecursive() } @@ -338,7 +295,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(out *jwriter.Writer, in AsyncMessage) { +func easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling2(out *jwriter.Writer, in AsyncMessage) { out.RawByte('{') first := true _ = first @@ -397,23 +354,23 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEve // MarshalJSON supports json.Marshaler interface func (v AsyncMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(&w, v) + easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling2(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v AsyncMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(w, v) + easyjson9289e183EncodeGithubComStrukturagNextcloudSpreedSignaling2(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *AsyncMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(&r, v) + easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AsyncMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2AsyncEvents2(l, v) + easyjson9289e183DecodeGithubComStrukturagNextcloudSpreedSignaling2(l, v) } diff --git a/talk/api.go b/api_backend.go similarity index 60% rename from talk/api.go rename to api_backend.go index 1bc0032..2fd0bc9 100644 --- a/talk/api.go +++ b/api_backend.go @@ -19,23 +19,21 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "crypto/hmac" + "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/hex" "encoding/json" "fmt" "net/http" + "net/url" "regexp" + "strings" "time" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" ) const ( @@ -51,6 +49,14 @@ const ( ConfigKeySessionPingLimit = "session-ping-limit" ) +func newRandomString(length int) string { + b := make([]byte, length/2) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return hex.EncodeToString(b) +} + func CalculateBackendChecksum(random string, body []byte, secret []byte) string { mac := hmac.New(sha256.New, secret) mac.Write([]byte(random)) // nolint @@ -60,7 +66,7 @@ func CalculateBackendChecksum(random string, body []byte, secret []byte) string func AddBackendChecksum(r *http.Request, body []byte, secret []byte) { // Add checksum so the backend can validate the request. - rnd := internal.RandomString(64) + rnd := newRandomString(64) checksum := CalculateBackendChecksum(rnd, body, secret) r.Header.Set(HeaderBackendSignalingRandom, rnd) r.Header.Set(HeaderBackendSignalingChecksum, checksum) @@ -80,8 +86,7 @@ func ValidateBackendChecksumValue(checksum string, random string, body []byte, s // Requests from Nextcloud to the signaling server. type BackendServerRoomRequest struct { - RoomId string `json:"-"` - Backend *Backend `json:"-"` + room *Room Type string `json:"type"` @@ -118,8 +123,8 @@ type BackendRoomInviteRequest struct { } type BackendRoomDisinviteRequest struct { - UserIds []string `json:"userids,omitempty"` - SessionIds []api.RoomSessionId `json:"sessionids,omitempty"` + UserIds []string `json:"userids,omitempty"` + SessionIds []string `json:"sessionids,omitempty"` // TODO(jojo): We should get rid of "AllUserIds" and find a better way to // notify existing users the room has changed and they need to update it. AllUserIds []string `json:"alluserids,omitempty"` @@ -137,26 +142,23 @@ type BackendRoomDeleteRequest struct { type BackendRoomInCallRequest struct { // TODO(jojo): Change "InCall" to "int" when #914 has landed in NC Talk. - InCall json.RawMessage `json:"incall,omitempty"` - All bool `json:"all,omitempty"` - Changed []api.StringMap `json:"changed,omitempty"` - Users []api.StringMap `json:"users,omitempty"` + InCall json.RawMessage `json:"incall,omitempty"` + All bool `json:"all,omitempty"` + Changed []map[string]interface{} `json:"changed,omitempty"` + Users []map[string]interface{} `json:"users,omitempty"` } type BackendRoomParticipantsRequest struct { - Changed []api.StringMap `json:"changed,omitempty"` - Users []api.StringMap `json:"users,omitempty"` + Changed []map[string]interface{} `json:"changed,omitempty"` + Users []map[string]interface{} `json:"users,omitempty"` } type BackendRoomMessageRequest struct { Data json.RawMessage `json:"data,omitempty"` } -type BackendRoomSwitchToSessionsList []api.RoomSessionId -type BackendRoomSwitchToSessionsMap map[api.RoomSessionId]json.RawMessage - -type BackendRoomSwitchToPublicSessionsList []api.PublicSessionId -type BackendRoomSwitchToPublicSessionsMap map[api.PublicSessionId]json.RawMessage +type BackendRoomSwitchToSessionsList []string +type BackendRoomSwitchToSessionsMap map[string]json.RawMessage type BackendRoomSwitchToMessageRequest struct { // Target room id @@ -170,8 +172,8 @@ type BackendRoomSwitchToMessageRequest struct { Sessions json.RawMessage `json:"sessions,omitempty"` // Internal properties - SessionsList BackendRoomSwitchToPublicSessionsList `json:"sessionslist,omitempty"` - SessionsMap BackendRoomSwitchToPublicSessionsMap `json:"sessionsmap,omitempty"` + SessionsList BackendRoomSwitchToSessionsList `json:"sessionslist,omitempty"` + SessionsMap BackendRoomSwitchToSessionsMap `json:"sessionsmap,omitempty"` } type BackendRoomDialoutRequest struct { @@ -189,26 +191,18 @@ func isValidNumber(s string) bool { return checkE164Number.MatchString(s) } -func (r *BackendRoomDialoutRequest) ValidateNumber() *api.Error { +func (r *BackendRoomDialoutRequest) ValidateNumber() *Error { if r.Number == "" { - return api.NewError("number_missing", "No number provided") + return NewError("number_missing", "No number provided") } if !isValidNumber(r.Number) { - return api.NewError("invalid_number", "Expected E.164 number.") + return NewError("invalid_number", "Expected E.164 number.") } return nil } -func (r *BackendRoomDialoutRequest) String() string { - data, err := json.Marshal(r) - if err != nil { - return fmt.Sprintf("Could not serialize %#v: %s", r, err) - } - return string(data) -} - type TransientAction string const ( @@ -219,7 +213,7 @@ const ( type BackendRoomTransientRequest struct { Action TransientAction `json:"action"` Key string `json:"key"` - Value any `json:"value,omitempty"` + Value interface{} `json:"value,omitempty"` TTL time.Duration `json:"ttl,omitempty"` } @@ -237,7 +231,7 @@ type BackendRoomDialoutError struct { type BackendRoomDialoutResponse struct { CallId string `json:"callid,omitempty"` - Error *api.Error `json:"error,omitempty"` + Error *Error `json:"error,omitempty"` } // Requests from the signaling server to the Nextcloud backend. @@ -278,7 +272,7 @@ type BackendClientResponse struct { Type string `json:"type"` - Error *api.Error `json:"error,omitempty"` + Error *Error `json:"error,omitempty"` Auth *BackendClientAuthResponse `json:"auth,omitempty"` @@ -296,11 +290,11 @@ type BackendClientAuthResponse struct { } type BackendClientRoomRequest struct { - Version string `json:"version"` - RoomId string `json:"roomid"` - Action string `json:"action,omitempty"` - UserId string `json:"userid"` - SessionId api.RoomSessionId `json:"sessionid"` + Version string `json:"version"` + RoomId string `json:"roomid"` + Action string `json:"action,omitempty"` + UserId string `json:"userid"` + SessionId string `json:"sessionid"` // For Nextcloud Talk with SIP support and for federated sessions. ActorId string `json:"actorid,omitempty"` @@ -308,17 +302,12 @@ type BackendClientRoomRequest struct { InCall int `json:"incall,omitempty"` } -type SessionWithUserData interface { - ClientType() api.ClientType - ParsedUserData() (api.StringMap, error) -} - -func (r *BackendClientRoomRequest) UpdateFromSession(s SessionWithUserData) { - if s.ClientType() == api.HelloClientTypeFederation { +func (r *BackendClientRoomRequest) UpdateFromSession(s Session) { + if s.ClientType() == HelloClientTypeFederation { // Need to send additional data for requests of federated users. if u, err := s.ParsedUserData(); err == nil && len(u) > 0 { - if actorType, found := api.GetStringMapEntry[string](u, "actorType"); found { - if actorId, found := api.GetStringMapEntry[string](u, "actorId"); found { + if actorType, found := getStringMapEntry[string](u, "actorType"); found { + if actorId, found := getStringMapEntry[string](u, "actorId"); found { r.ActorId = actorId r.ActorType = actorType } @@ -327,7 +316,7 @@ func (r *BackendClientRoomRequest) UpdateFromSession(s SessionWithUserData) { } } -func NewBackendClientRoomRequest(roomid string, userid string, sessionid api.RoomSessionId) *BackendClientRequest { +func NewBackendClientRoomRequest(roomid string, userid string, sessionid string) *BackendClientRequest { return &BackendClientRequest{ Type: "room", Room: &BackendClientRoomRequest{ @@ -349,7 +338,7 @@ type BackendClientRoomResponse struct { // See "RoomSessionData" for a possible content. Session json.RawMessage `json:"session,omitempty"` - Permissions *[]api.Permission `json:"permissions,omitempty"` + Permissions *[]Permission `json:"permissions,omitempty"` } type RoomSessionData struct { @@ -357,8 +346,8 @@ type RoomSessionData struct { } type BackendPingEntry struct { - UserId string `json:"userid,omitempty"` - SessionId api.RoomSessionId `json:"sessionid"` + UserId string `json:"userid,omitempty"` + SessionId string `json:"sessionid"` } type BackendClientPingRequest struct { @@ -384,12 +373,12 @@ type BackendClientRingResponse struct { } type BackendClientSessionRequest struct { - Version string `json:"version"` - RoomId string `json:"roomid"` - Action string `json:"action"` - SessionId api.PublicSessionId `json:"sessionid"` - UserId string `json:"userid,omitempty"` - User json.RawMessage `json:"user,omitempty"` + Version string `json:"version"` + RoomId string `json:"roomid"` + Action string `json:"action"` + SessionId string `json:"sessionid"` + UserId string `json:"userid,omitempty"` + User json.RawMessage `json:"user,omitempty"` } type BackendClientSessionResponse struct { @@ -397,7 +386,7 @@ type BackendClientSessionResponse struct { RoomId string `json:"roomid"` } -func NewBackendClientSessionRequest(roomid string, action string, sessionid api.PublicSessionId, msg *api.AddSessionInternalClientMessage) *BackendClientRequest { +func NewBackendClientSessionRequest(roomid string, action string, sessionid string, msg *AddSessionInternalClientMessage) *BackendClientRequest { request := &BackendClientRequest{ Type: "session", Session: &BackendClientSessionRequest{ @@ -414,6 +403,24 @@ func NewBackendClientSessionRequest(roomid string, action string, sessionid api. return request } +type OcsMeta struct { + Status string `json:"status"` + StatusCode int `json:"statuscode"` + Message string `json:"message"` +} + +type OcsBody struct { + Meta OcsMeta `json:"meta"` + Data json.RawMessage `json:"data"` +} + +type OcsResponse struct { + json.Marshaler + json.Unmarshaler + + Ocs *OcsBody `json:"ocs"` +} + // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 type TurnCredentials struct { Username string `json:"username"` @@ -422,107 +429,38 @@ type TurnCredentials struct { URIs []string `json:"uris"` } -type BackendServerInfoVideoRoom struct { - Name string `json:"name,omitempty"` - Version string `json:"version,omitempty"` - Author string `json:"author,omitempty"` +// Information on a backend in the etcd cluster. + +type BackendInformationEtcd struct { + parsedUrl *url.URL + + Url string `json:"url"` + Secret string `json:"secret"` + + MaxStreamBitrate int `json:"maxstreambitrate,omitempty"` + MaxScreenBitrate int `json:"maxscreenbitrate,omitempty"` + + SessionLimit uint64 `json:"sessionlimit,omitempty"` } -type BackendServerInfoSfuJanus struct { - Url string `json:"url"` +func (p *BackendInformationEtcd) CheckValid() error { + if p.Url == "" { + return fmt.Errorf("url missing") + } + if p.Secret == "" { + return fmt.Errorf("secret missing") + } - Connected bool `json:"connected"` + parsedUrl, err := url.Parse(p.Url) + if err != nil { + return fmt.Errorf("invalid url: %w", err) + } - Name string `json:"name,omitempty"` - Version string `json:"version,omitempty"` - Author string `json:"author,omitempty"` + if strings.Contains(parsedUrl.Host, ":") && hasStandardPort(parsedUrl) { + parsedUrl.Host = parsedUrl.Hostname() + p.Url = parsedUrl.String() + } - DataChannels *bool `json:"datachannels,omitempty"` - FullTrickle *bool `json:"fulltrickle,omitempty"` - LocalIP string `json:"localip,omitempty"` - IPv6 *bool `json:"ipv6,omitempty"` - - VideoRoom *BackendServerInfoVideoRoom `json:"videoroom,omitempty"` -} - -type BackendServerInfoSfuProxyBandwidth struct { - // Incoming is the bandwidth utilization for publishers in percent. - Incoming *float64 `json:"incoming,omitempty"` - // Outgoing is the bandwidth utilization for subscribers in percent. - Outgoing *float64 `json:"outgoing,omitempty"` - - // Received is the incoming bandwidth. - Received api.Bandwidth `json:"received,omitempty"` - // Sent is the outgoing bandwidth. - Sent api.Bandwidth `json:"sent,omitempty"` -} - -type BackendServerInfoSfuProxy struct { - Url string `json:"url"` - IP string `json:"ip,omitempty"` - - Connected bool `json:"connected"` - Temporary bool `json:"temporary"` - Shutdown *bool `json:"shutdown,omitempty"` - Uptime *time.Time `json:"uptime,omitempty"` - - Version string `json:"version,omitempty"` - Features []string `json:"features,omitempty"` - - Country geoip.Country `json:"country,omitempty"` - Load *uint64 `json:"load,omitempty"` - Bandwidth *BackendServerInfoSfuProxyBandwidth `json:"bandwidth,omitempty"` -} - -type SfuMode string - -const ( - SfuModeJanus SfuMode = "janus" - SfuModeProxy SfuMode = "proxy" -) - -type BackendServerInfoSfu struct { - Mode SfuMode `json:"mode"` - - Janus *BackendServerInfoSfuJanus `json:"janus,omitempty"` - Proxies []BackendServerInfoSfuProxy `json:"proxies,omitempty"` -} - -type BackendServerInfoDialout struct { - SessionId api.PublicSessionId `json:"sessionid"` - Connected bool `json:"connected"` - Address string `json:"address,omitempty"` - UserAgent string `json:"useragent,omitempty"` - Version string `json:"version,omitempty"` - Features []string `json:"features,omitempty"` -} - -type BackendServerInfoNats struct { - Urls []string `json:"urls"` - Connected bool `json:"connected"` - - ServerUrl string `json:"serverurl,omitempty"` - ServerID string `json:"serverid,omitempty"` - ServerVersion string `json:"version,omitempty"` - ClusterName string `json:"clustername,omitempty"` -} - -type BackendServerInfoGrpc struct { - Target string `json:"target"` - IP string `json:"ip,omitempty"` - Connected bool `json:"connected"` - - Version string `json:"version,omitempty"` -} - -type BackendServerInfo struct { - Version string `json:"version"` - Features []string `json:"features"` - - Sfu *BackendServerInfoSfu `json:"sfu,omitempty"` - Dialout []BackendServerInfoDialout `json:"dialout,omitempty"` - - Nats *BackendServerInfoNats `json:"nats,omitempty"` - Grpc []BackendServerInfoGrpc `json:"grpc,omitempty"` - Etcd *etcd.BackendServerInfoEtcd `json:"etcd,omitempty"` + p.parsedUrl = parsedUrl + return nil } diff --git a/api_backend_easyjson.go b/api_backend_easyjson.go new file mode 100644 index 0000000..95338f0 --- /dev/null +++ b/api_backend_easyjson.go @@ -0,0 +1,3554 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package signaling + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" + time "time" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling(in *jlexer.Lexer, out *TurnCredentials) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "username": + out.Username = string(in.String()) + case "password": + out.Password = string(in.String()) + case "ttl": + out.TTL = int64(in.Int64()) + case "uris": + if in.IsNull() { + in.Skip() + out.URIs = nil + } else { + in.Delim('[') + if out.URIs == nil { + if !in.IsDelim(']') { + out.URIs = make([]string, 0, 4) + } else { + out.URIs = []string{} + } + } else { + out.URIs = (out.URIs)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.URIs = append(out.URIs, v1) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling(out *jwriter.Writer, in TurnCredentials) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"username\":" + out.RawString(prefix[1:]) + out.String(string(in.Username)) + } + { + const prefix string = ",\"password\":" + out.RawString(prefix) + out.String(string(in.Password)) + } + { + const prefix string = ",\"ttl\":" + out.RawString(prefix) + out.Int64(int64(in.TTL)) + } + { + const prefix string = ",\"uris\":" + out.RawString(prefix) + if in.URIs == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v2, v3 := range in.URIs { + if v2 > 0 { + out.RawByte(',') + } + out.String(string(v3)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v TurnCredentials) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v TurnCredentials) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *TurnCredentials) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *TurnCredentials) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling1(in *jlexer.Lexer, out *RoomSessionData) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "userid": + out.UserId = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling1(out *jwriter.Writer, in RoomSessionData) { + out.RawByte('{') + first := true + _ = first + if in.UserId != "" { + const prefix string = ",\"userid\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.UserId)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v RoomSessionData) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v RoomSessionData) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *RoomSessionData) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *RoomSessionData) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling1(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlexer.Lexer, out *OcsResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "ocs": + if in.IsNull() { + in.Skip() + out.Ocs = nil + } else { + if out.Ocs == nil { + out.Ocs = new(OcsBody) + } + (*out.Ocs).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling2(out *jwriter.Writer, in OcsResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"ocs\":" + out.RawString(prefix[1:]) + if in.Ocs == nil { + out.RawString("null") + } else { + (*in.Ocs).MarshalEasyJSON(out) + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v OcsResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v OcsResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *OcsResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *OcsResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling2(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling3(in *jlexer.Lexer, out *OcsMeta) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "status": + out.Status = string(in.String()) + case "statuscode": + out.StatusCode = int(in.Int()) + case "message": + out.Message = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling3(out *jwriter.Writer, in OcsMeta) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"status\":" + out.RawString(prefix[1:]) + out.String(string(in.Status)) + } + { + const prefix string = ",\"statuscode\":" + out.RawString(prefix) + out.Int(int(in.StatusCode)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v OcsMeta) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v OcsMeta) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *OcsMeta) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *OcsMeta) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling3(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling4(in *jlexer.Lexer, out *OcsBody) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "meta": + (out.Meta).UnmarshalEasyJSON(in) + case "data": + if data := in.Raw(); in.Ok() { + in.AddError((out.Data).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling4(out *jwriter.Writer, in OcsBody) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"meta\":" + out.RawString(prefix[1:]) + (in.Meta).MarshalEasyJSON(out) + } + { + const prefix string = ",\"data\":" + out.RawString(prefix) + out.Raw((in.Data).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v OcsBody) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling4(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v OcsBody) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling4(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *OcsBody) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling4(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *OcsBody) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling4(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling5(in *jlexer.Lexer, out *BackendServerRoomResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "dialout": + if in.IsNull() { + in.Skip() + out.Dialout = nil + } else { + if out.Dialout == nil { + out.Dialout = new(BackendRoomDialoutResponse) + } + (*out.Dialout).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling5(out *jwriter.Writer, in BackendServerRoomResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + if in.Dialout != nil { + const prefix string = ",\"dialout\":" + out.RawString(prefix) + (*in.Dialout).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendServerRoomResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling5(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendServerRoomResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling5(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendServerRoomResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling5(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendServerRoomResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling5(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling6(in *jlexer.Lexer, out *BackendServerRoomRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "invite": + if in.IsNull() { + in.Skip() + out.Invite = nil + } else { + if out.Invite == nil { + out.Invite = new(BackendRoomInviteRequest) + } + (*out.Invite).UnmarshalEasyJSON(in) + } + case "disinvite": + if in.IsNull() { + in.Skip() + out.Disinvite = nil + } else { + if out.Disinvite == nil { + out.Disinvite = new(BackendRoomDisinviteRequest) + } + (*out.Disinvite).UnmarshalEasyJSON(in) + } + case "update": + if in.IsNull() { + in.Skip() + out.Update = nil + } else { + if out.Update == nil { + out.Update = new(BackendRoomUpdateRequest) + } + (*out.Update).UnmarshalEasyJSON(in) + } + case "delete": + if in.IsNull() { + in.Skip() + out.Delete = nil + } else { + if out.Delete == nil { + out.Delete = new(BackendRoomDeleteRequest) + } + (*out.Delete).UnmarshalEasyJSON(in) + } + case "incall": + if in.IsNull() { + in.Skip() + out.InCall = nil + } else { + if out.InCall == nil { + out.InCall = new(BackendRoomInCallRequest) + } + (*out.InCall).UnmarshalEasyJSON(in) + } + case "participants": + if in.IsNull() { + in.Skip() + out.Participants = nil + } else { + if out.Participants == nil { + out.Participants = new(BackendRoomParticipantsRequest) + } + (*out.Participants).UnmarshalEasyJSON(in) + } + case "message": + if in.IsNull() { + in.Skip() + out.Message = nil + } else { + if out.Message == nil { + out.Message = new(BackendRoomMessageRequest) + } + (*out.Message).UnmarshalEasyJSON(in) + } + case "switchto": + if in.IsNull() { + in.Skip() + out.SwitchTo = nil + } else { + if out.SwitchTo == nil { + out.SwitchTo = new(BackendRoomSwitchToMessageRequest) + } + (*out.SwitchTo).UnmarshalEasyJSON(in) + } + case "dialout": + if in.IsNull() { + in.Skip() + out.Dialout = nil + } else { + if out.Dialout == nil { + out.Dialout = new(BackendRoomDialoutRequest) + } + (*out.Dialout).UnmarshalEasyJSON(in) + } + case "transient": + if in.IsNull() { + in.Skip() + out.Transient = nil + } else { + if out.Transient == nil { + out.Transient = new(BackendRoomTransientRequest) + } + (*out.Transient).UnmarshalEasyJSON(in) + } + case "received": + out.ReceivedTime = int64(in.Int64()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling6(out *jwriter.Writer, in BackendServerRoomRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + if in.Invite != nil { + const prefix string = ",\"invite\":" + out.RawString(prefix) + (*in.Invite).MarshalEasyJSON(out) + } + if in.Disinvite != nil { + const prefix string = ",\"disinvite\":" + out.RawString(prefix) + (*in.Disinvite).MarshalEasyJSON(out) + } + if in.Update != nil { + const prefix string = ",\"update\":" + out.RawString(prefix) + (*in.Update).MarshalEasyJSON(out) + } + if in.Delete != nil { + const prefix string = ",\"delete\":" + out.RawString(prefix) + (*in.Delete).MarshalEasyJSON(out) + } + if in.InCall != nil { + const prefix string = ",\"incall\":" + out.RawString(prefix) + (*in.InCall).MarshalEasyJSON(out) + } + if in.Participants != nil { + const prefix string = ",\"participants\":" + out.RawString(prefix) + (*in.Participants).MarshalEasyJSON(out) + } + if in.Message != nil { + const prefix string = ",\"message\":" + out.RawString(prefix) + (*in.Message).MarshalEasyJSON(out) + } + if in.SwitchTo != nil { + const prefix string = ",\"switchto\":" + out.RawString(prefix) + (*in.SwitchTo).MarshalEasyJSON(out) + } + if in.Dialout != nil { + const prefix string = ",\"dialout\":" + out.RawString(prefix) + (*in.Dialout).MarshalEasyJSON(out) + } + if in.Transient != nil { + const prefix string = ",\"transient\":" + out.RawString(prefix) + (*in.Transient).MarshalEasyJSON(out) + } + if in.ReceivedTime != 0 { + const prefix string = ",\"received\":" + out.RawString(prefix) + out.Int64(int64(in.ReceivedTime)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendServerRoomRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling6(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendServerRoomRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling6(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendServerRoomRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling6(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendServerRoomRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling6(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling7(in *jlexer.Lexer, out *BackendRoomUpdateRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "userids": + if in.IsNull() { + in.Skip() + out.UserIds = nil + } else { + in.Delim('[') + if out.UserIds == nil { + if !in.IsDelim(']') { + out.UserIds = make([]string, 0, 4) + } else { + out.UserIds = []string{} + } + } else { + out.UserIds = (out.UserIds)[:0] + } + for !in.IsDelim(']') { + var v4 string + v4 = string(in.String()) + out.UserIds = append(out.UserIds, v4) + in.WantComma() + } + in.Delim(']') + } + case "properties": + if data := in.Raw(); in.Ok() { + in.AddError((out.Properties).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling7(out *jwriter.Writer, in BackendRoomUpdateRequest) { + out.RawByte('{') + first := true + _ = first + if len(in.UserIds) != 0 { + const prefix string = ",\"userids\":" + first = false + out.RawString(prefix[1:]) + { + out.RawByte('[') + for v5, v6 := range in.UserIds { + if v5 > 0 { + out.RawByte(',') + } + out.String(string(v6)) + } + out.RawByte(']') + } + } + if len(in.Properties) != 0 { + const prefix string = ",\"properties\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Raw((in.Properties).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomUpdateRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling7(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomUpdateRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling7(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomUpdateRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling7(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomUpdateRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling7(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling8(in *jlexer.Lexer, out *BackendRoomTransientRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "action": + out.Action = TransientAction(in.String()) + case "key": + out.Key = string(in.String()) + case "value": + if m, ok := out.Value.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := out.Value.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + out.Value = in.Interface() + } + case "ttl": + out.TTL = time.Duration(in.Int64()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling8(out *jwriter.Writer, in BackendRoomTransientRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"action\":" + out.RawString(prefix[1:]) + out.String(string(in.Action)) + } + { + const prefix string = ",\"key\":" + out.RawString(prefix) + out.String(string(in.Key)) + } + if in.Value != nil { + const prefix string = ",\"value\":" + out.RawString(prefix) + if m, ok := in.Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := in.Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(in.Value)) + } + } + if in.TTL != 0 { + const prefix string = ",\"ttl\":" + out.RawString(prefix) + out.Int64(int64(in.TTL)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomTransientRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling8(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomTransientRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling8(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomTransientRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling8(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomTransientRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling8(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling9(in *jlexer.Lexer, out *BackendRoomSwitchToMessageRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "roomid": + out.RoomId = string(in.String()) + case "sessions": + if data := in.Raw(); in.Ok() { + in.AddError((out.Sessions).UnmarshalJSON(data)) + } + case "sessionslist": + if in.IsNull() { + in.Skip() + out.SessionsList = nil + } else { + in.Delim('[') + if out.SessionsList == nil { + if !in.IsDelim(']') { + out.SessionsList = make(BackendRoomSwitchToSessionsList, 0, 4) + } else { + out.SessionsList = BackendRoomSwitchToSessionsList{} + } + } else { + out.SessionsList = (out.SessionsList)[:0] + } + for !in.IsDelim(']') { + var v7 string + v7 = string(in.String()) + out.SessionsList = append(out.SessionsList, v7) + in.WantComma() + } + in.Delim(']') + } + case "sessionsmap": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + out.SessionsMap = make(BackendRoomSwitchToSessionsMap) + } else { + out.SessionsMap = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v8 json.RawMessage + if data := in.Raw(); in.Ok() { + in.AddError((v8).UnmarshalJSON(data)) + } + (out.SessionsMap)[key] = v8 + in.WantComma() + } + in.Delim('}') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling9(out *jwriter.Writer, in BackendRoomSwitchToMessageRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"roomid\":" + out.RawString(prefix[1:]) + out.String(string(in.RoomId)) + } + if len(in.Sessions) != 0 { + const prefix string = ",\"sessions\":" + out.RawString(prefix) + out.Raw((in.Sessions).MarshalJSON()) + } + if len(in.SessionsList) != 0 { + const prefix string = ",\"sessionslist\":" + out.RawString(prefix) + { + out.RawByte('[') + for v9, v10 := range in.SessionsList { + if v9 > 0 { + out.RawByte(',') + } + out.String(string(v10)) + } + out.RawByte(']') + } + } + if len(in.SessionsMap) != 0 { + const prefix string = ",\"sessionsmap\":" + out.RawString(prefix) + { + out.RawByte('{') + v11First := true + for v11Name, v11Value := range in.SessionsMap { + if v11First { + v11First = false + } else { + out.RawByte(',') + } + out.String(string(v11Name)) + out.RawByte(':') + out.Raw((v11Value).MarshalJSON()) + } + out.RawByte('}') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomSwitchToMessageRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling9(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomSwitchToMessageRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling9(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomSwitchToMessageRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling9(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomSwitchToMessageRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling9(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling10(in *jlexer.Lexer, out *BackendRoomParticipantsRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "changed": + if in.IsNull() { + in.Skip() + out.Changed = nil + } else { + in.Delim('[') + if out.Changed == nil { + if !in.IsDelim(']') { + out.Changed = make([]map[string]interface{}, 0, 8) + } else { + out.Changed = []map[string]interface{}{} + } + } else { + out.Changed = (out.Changed)[:0] + } + for !in.IsDelim(']') { + var v12 map[string]interface{} + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + v12 = make(map[string]interface{}) + } else { + v12 = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v13 interface{} + if m, ok := v13.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := v13.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + v13 = in.Interface() + } + (v12)[key] = v13 + in.WantComma() + } + in.Delim('}') + } + out.Changed = append(out.Changed, v12) + in.WantComma() + } + in.Delim(']') + } + case "users": + if in.IsNull() { + in.Skip() + out.Users = nil + } else { + in.Delim('[') + if out.Users == nil { + if !in.IsDelim(']') { + out.Users = make([]map[string]interface{}, 0, 8) + } else { + out.Users = []map[string]interface{}{} + } + } else { + out.Users = (out.Users)[:0] + } + for !in.IsDelim(']') { + var v14 map[string]interface{} + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + v14 = make(map[string]interface{}) + } else { + v14 = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v15 interface{} + if m, ok := v15.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := v15.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + v15 = in.Interface() + } + (v14)[key] = v15 + in.WantComma() + } + in.Delim('}') + } + out.Users = append(out.Users, v14) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling10(out *jwriter.Writer, in BackendRoomParticipantsRequest) { + out.RawByte('{') + first := true + _ = first + if len(in.Changed) != 0 { + const prefix string = ",\"changed\":" + first = false + out.RawString(prefix[1:]) + { + out.RawByte('[') + for v16, v17 := range in.Changed { + if v16 > 0 { + out.RawByte(',') + } + if v17 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) + } else { + out.RawByte('{') + v18First := true + for v18Name, v18Value := range v17 { + if v18First { + v18First = false + } else { + out.RawByte(',') + } + out.String(string(v18Name)) + out.RawByte(':') + if m, ok := v18Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v18Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v18Value)) + } + } + out.RawByte('}') + } + } + out.RawByte(']') + } + } + if len(in.Users) != 0 { + const prefix string = ",\"users\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('[') + for v19, v20 := range in.Users { + if v19 > 0 { + out.RawByte(',') + } + if v20 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) + } else { + out.RawByte('{') + v21First := true + for v21Name, v21Value := range v20 { + if v21First { + v21First = false + } else { + out.RawByte(',') + } + out.String(string(v21Name)) + out.RawByte(':') + if m, ok := v21Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v21Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v21Value)) + } + } + out.RawByte('}') + } + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomParticipantsRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling10(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomParticipantsRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling10(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomParticipantsRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling10(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomParticipantsRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling10(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling11(in *jlexer.Lexer, out *BackendRoomMessageRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "data": + if data := in.Raw(); in.Ok() { + in.AddError((out.Data).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling11(out *jwriter.Writer, in BackendRoomMessageRequest) { + out.RawByte('{') + first := true + _ = first + if len(in.Data) != 0 { + const prefix string = ",\"data\":" + first = false + out.RawString(prefix[1:]) + out.Raw((in.Data).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomMessageRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling11(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomMessageRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling11(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomMessageRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling11(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomMessageRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling11(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling12(in *jlexer.Lexer, out *BackendRoomInviteRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "userids": + if in.IsNull() { + in.Skip() + out.UserIds = nil + } else { + in.Delim('[') + if out.UserIds == nil { + if !in.IsDelim(']') { + out.UserIds = make([]string, 0, 4) + } else { + out.UserIds = []string{} + } + } else { + out.UserIds = (out.UserIds)[:0] + } + for !in.IsDelim(']') { + var v22 string + v22 = string(in.String()) + out.UserIds = append(out.UserIds, v22) + in.WantComma() + } + in.Delim(']') + } + case "alluserids": + if in.IsNull() { + in.Skip() + out.AllUserIds = nil + } else { + in.Delim('[') + if out.AllUserIds == nil { + if !in.IsDelim(']') { + out.AllUserIds = make([]string, 0, 4) + } else { + out.AllUserIds = []string{} + } + } else { + out.AllUserIds = (out.AllUserIds)[:0] + } + for !in.IsDelim(']') { + var v23 string + v23 = string(in.String()) + out.AllUserIds = append(out.AllUserIds, v23) + in.WantComma() + } + in.Delim(']') + } + case "properties": + if data := in.Raw(); in.Ok() { + in.AddError((out.Properties).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling12(out *jwriter.Writer, in BackendRoomInviteRequest) { + out.RawByte('{') + first := true + _ = first + if len(in.UserIds) != 0 { + const prefix string = ",\"userids\":" + first = false + out.RawString(prefix[1:]) + { + out.RawByte('[') + for v24, v25 := range in.UserIds { + if v24 > 0 { + out.RawByte(',') + } + out.String(string(v25)) + } + out.RawByte(']') + } + } + if len(in.AllUserIds) != 0 { + const prefix string = ",\"alluserids\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('[') + for v26, v27 := range in.AllUserIds { + if v26 > 0 { + out.RawByte(',') + } + out.String(string(v27)) + } + out.RawByte(']') + } + } + if len(in.Properties) != 0 { + const prefix string = ",\"properties\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Raw((in.Properties).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomInviteRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling12(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomInviteRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling12(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomInviteRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling12(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomInviteRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling12(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling13(in *jlexer.Lexer, out *BackendRoomInCallRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "incall": + if data := in.Raw(); in.Ok() { + in.AddError((out.InCall).UnmarshalJSON(data)) + } + case "all": + out.All = bool(in.Bool()) + case "changed": + if in.IsNull() { + in.Skip() + out.Changed = nil + } else { + in.Delim('[') + if out.Changed == nil { + if !in.IsDelim(']') { + out.Changed = make([]map[string]interface{}, 0, 8) + } else { + out.Changed = []map[string]interface{}{} + } + } else { + out.Changed = (out.Changed)[:0] + } + for !in.IsDelim(']') { + var v28 map[string]interface{} + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + v28 = make(map[string]interface{}) + } else { + v28 = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v29 interface{} + if m, ok := v29.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := v29.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + v29 = in.Interface() + } + (v28)[key] = v29 + in.WantComma() + } + in.Delim('}') + } + out.Changed = append(out.Changed, v28) + in.WantComma() + } + in.Delim(']') + } + case "users": + if in.IsNull() { + in.Skip() + out.Users = nil + } else { + in.Delim('[') + if out.Users == nil { + if !in.IsDelim(']') { + out.Users = make([]map[string]interface{}, 0, 8) + } else { + out.Users = []map[string]interface{}{} + } + } else { + out.Users = (out.Users)[:0] + } + for !in.IsDelim(']') { + var v30 map[string]interface{} + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + if !in.IsDelim('}') { + v30 = make(map[string]interface{}) + } else { + v30 = nil + } + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v31 interface{} + if m, ok := v31.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := v31.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + v31 = in.Interface() + } + (v30)[key] = v31 + in.WantComma() + } + in.Delim('}') + } + out.Users = append(out.Users, v30) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling13(out *jwriter.Writer, in BackendRoomInCallRequest) { + out.RawByte('{') + first := true + _ = first + if len(in.InCall) != 0 { + const prefix string = ",\"incall\":" + first = false + out.RawString(prefix[1:]) + out.Raw((in.InCall).MarshalJSON()) + } + if in.All { + const prefix string = ",\"all\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Bool(bool(in.All)) + } + if len(in.Changed) != 0 { + const prefix string = ",\"changed\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('[') + for v32, v33 := range in.Changed { + if v32 > 0 { + out.RawByte(',') + } + if v33 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) + } else { + out.RawByte('{') + v34First := true + for v34Name, v34Value := range v33 { + if v34First { + v34First = false + } else { + out.RawByte(',') + } + out.String(string(v34Name)) + out.RawByte(':') + if m, ok := v34Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v34Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v34Value)) + } + } + out.RawByte('}') + } + } + out.RawByte(']') + } + } + if len(in.Users) != 0 { + const prefix string = ",\"users\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('[') + for v35, v36 := range in.Users { + if v35 > 0 { + out.RawByte(',') + } + if v36 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) + } else { + out.RawByte('{') + v37First := true + for v37Name, v37Value := range v36 { + if v37First { + v37First = false + } else { + out.RawByte(',') + } + out.String(string(v37Name)) + out.RawByte(':') + if m, ok := v37Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v37Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v37Value)) + } + } + out.RawByte('}') + } + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomInCallRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling13(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomInCallRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling13(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomInCallRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling13(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomInCallRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling13(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling14(in *jlexer.Lexer, out *BackendRoomDisinviteRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "userids": + if in.IsNull() { + in.Skip() + out.UserIds = nil + } else { + in.Delim('[') + if out.UserIds == nil { + if !in.IsDelim(']') { + out.UserIds = make([]string, 0, 4) + } else { + out.UserIds = []string{} + } + } else { + out.UserIds = (out.UserIds)[:0] + } + for !in.IsDelim(']') { + var v38 string + v38 = string(in.String()) + out.UserIds = append(out.UserIds, v38) + in.WantComma() + } + in.Delim(']') + } + case "sessionids": + if in.IsNull() { + in.Skip() + out.SessionIds = nil + } else { + in.Delim('[') + if out.SessionIds == nil { + if !in.IsDelim(']') { + out.SessionIds = make([]string, 0, 4) + } else { + out.SessionIds = []string{} + } + } else { + out.SessionIds = (out.SessionIds)[:0] + } + for !in.IsDelim(']') { + var v39 string + v39 = string(in.String()) + out.SessionIds = append(out.SessionIds, v39) + in.WantComma() + } + in.Delim(']') + } + case "alluserids": + if in.IsNull() { + in.Skip() + out.AllUserIds = nil + } else { + in.Delim('[') + if out.AllUserIds == nil { + if !in.IsDelim(']') { + out.AllUserIds = make([]string, 0, 4) + } else { + out.AllUserIds = []string{} + } + } else { + out.AllUserIds = (out.AllUserIds)[:0] + } + for !in.IsDelim(']') { + var v40 string + v40 = string(in.String()) + out.AllUserIds = append(out.AllUserIds, v40) + in.WantComma() + } + in.Delim(']') + } + case "properties": + if data := in.Raw(); in.Ok() { + in.AddError((out.Properties).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling14(out *jwriter.Writer, in BackendRoomDisinviteRequest) { + out.RawByte('{') + first := true + _ = first + if len(in.UserIds) != 0 { + const prefix string = ",\"userids\":" + first = false + out.RawString(prefix[1:]) + { + out.RawByte('[') + for v41, v42 := range in.UserIds { + if v41 > 0 { + out.RawByte(',') + } + out.String(string(v42)) + } + out.RawByte(']') + } + } + if len(in.SessionIds) != 0 { + const prefix string = ",\"sessionids\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('[') + for v43, v44 := range in.SessionIds { + if v43 > 0 { + out.RawByte(',') + } + out.String(string(v44)) + } + out.RawByte(']') + } + } + if len(in.AllUserIds) != 0 { + const prefix string = ",\"alluserids\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + { + out.RawByte('[') + for v45, v46 := range in.AllUserIds { + if v45 > 0 { + out.RawByte(',') + } + out.String(string(v46)) + } + out.RawByte(']') + } + } + if len(in.Properties) != 0 { + const prefix string = ",\"properties\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Raw((in.Properties).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomDisinviteRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling14(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomDisinviteRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling14(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomDisinviteRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling14(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomDisinviteRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling14(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling15(in *jlexer.Lexer, out *BackendRoomDialoutResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "callid": + out.CallId = string(in.String()) + case "error": + if in.IsNull() { + in.Skip() + out.Error = nil + } else { + if out.Error == nil { + out.Error = new(Error) + } + (*out.Error).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling15(out *jwriter.Writer, in BackendRoomDialoutResponse) { + out.RawByte('{') + first := true + _ = first + if in.CallId != "" { + const prefix string = ",\"callid\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.CallId)) + } + if in.Error != nil { + const prefix string = ",\"error\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + (*in.Error).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomDialoutResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling15(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomDialoutResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling15(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomDialoutResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling15(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomDialoutResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling15(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling16(in *jlexer.Lexer, out *BackendRoomDialoutRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "number": + out.Number = string(in.String()) + case "options": + if data := in.Raw(); in.Ok() { + in.AddError((out.Options).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling16(out *jwriter.Writer, in BackendRoomDialoutRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"number\":" + out.RawString(prefix[1:]) + out.String(string(in.Number)) + } + if len(in.Options) != 0 { + const prefix string = ",\"options\":" + out.RawString(prefix) + out.Raw((in.Options).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomDialoutRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling16(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomDialoutRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling16(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomDialoutRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling16(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomDialoutRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling16(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling17(in *jlexer.Lexer, out *BackendRoomDialoutError) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "code": + out.Code = string(in.String()) + case "message": + out.Message = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling17(out *jwriter.Writer, in BackendRoomDialoutError) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"code\":" + out.RawString(prefix[1:]) + out.String(string(in.Code)) + } + if in.Message != "" { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomDialoutError) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling17(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomDialoutError) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling17(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomDialoutError) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling17(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomDialoutError) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling17(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling18(in *jlexer.Lexer, out *BackendRoomDeleteRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "userids": + if in.IsNull() { + in.Skip() + out.UserIds = nil + } else { + in.Delim('[') + if out.UserIds == nil { + if !in.IsDelim(']') { + out.UserIds = make([]string, 0, 4) + } else { + out.UserIds = []string{} + } + } else { + out.UserIds = (out.UserIds)[:0] + } + for !in.IsDelim(']') { + var v47 string + v47 = string(in.String()) + out.UserIds = append(out.UserIds, v47) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling18(out *jwriter.Writer, in BackendRoomDeleteRequest) { + out.RawByte('{') + first := true + _ = first + if len(in.UserIds) != 0 { + const prefix string = ",\"userids\":" + first = false + out.RawString(prefix[1:]) + { + out.RawByte('[') + for v48, v49 := range in.UserIds { + if v48 > 0 { + out.RawByte(',') + } + out.String(string(v49)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendRoomDeleteRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling18(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendRoomDeleteRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling18(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendRoomDeleteRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling18(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendRoomDeleteRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling18(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling19(in *jlexer.Lexer, out *BackendPingEntry) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "userid": + out.UserId = string(in.String()) + case "sessionid": + out.SessionId = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling19(out *jwriter.Writer, in BackendPingEntry) { + out.RawByte('{') + first := true + _ = first + if in.UserId != "" { + const prefix string = ",\"userid\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.UserId)) + } + { + const prefix string = ",\"sessionid\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.SessionId)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendPingEntry) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling19(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendPingEntry) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling19(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendPingEntry) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling19(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendPingEntry) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling19(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling20(in *jlexer.Lexer, out *BackendInformationEtcd) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "url": + out.Url = string(in.String()) + case "secret": + out.Secret = string(in.String()) + case "maxstreambitrate": + out.MaxStreamBitrate = int(in.Int()) + case "maxscreenbitrate": + out.MaxScreenBitrate = int(in.Int()) + case "sessionlimit": + out.SessionLimit = uint64(in.Uint64()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling20(out *jwriter.Writer, in BackendInformationEtcd) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"url\":" + out.RawString(prefix[1:]) + out.String(string(in.Url)) + } + { + const prefix string = ",\"secret\":" + out.RawString(prefix) + out.String(string(in.Secret)) + } + if in.MaxStreamBitrate != 0 { + const prefix string = ",\"maxstreambitrate\":" + out.RawString(prefix) + out.Int(int(in.MaxStreamBitrate)) + } + if in.MaxScreenBitrate != 0 { + const prefix string = ",\"maxscreenbitrate\":" + out.RawString(prefix) + out.Int(int(in.MaxScreenBitrate)) + } + if in.SessionLimit != 0 { + const prefix string = ",\"sessionlimit\":" + out.RawString(prefix) + out.Uint64(uint64(in.SessionLimit)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendInformationEtcd) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling20(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendInformationEtcd) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling20(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendInformationEtcd) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling20(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendInformationEtcd) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling20(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling21(in *jlexer.Lexer, out *BackendClientSessionResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "version": + out.Version = string(in.String()) + case "roomid": + out.RoomId = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling21(out *jwriter.Writer, in BackendClientSessionResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"version\":" + out.RawString(prefix[1:]) + out.String(string(in.Version)) + } + { + const prefix string = ",\"roomid\":" + out.RawString(prefix) + out.String(string(in.RoomId)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientSessionResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling21(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientSessionResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling21(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientSessionResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling21(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientSessionResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling21(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling22(in *jlexer.Lexer, out *BackendClientSessionRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "version": + out.Version = string(in.String()) + case "roomid": + out.RoomId = string(in.String()) + case "action": + out.Action = string(in.String()) + case "sessionid": + out.SessionId = string(in.String()) + case "userid": + out.UserId = string(in.String()) + case "user": + if data := in.Raw(); in.Ok() { + in.AddError((out.User).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling22(out *jwriter.Writer, in BackendClientSessionRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"version\":" + out.RawString(prefix[1:]) + out.String(string(in.Version)) + } + { + const prefix string = ",\"roomid\":" + out.RawString(prefix) + out.String(string(in.RoomId)) + } + { + const prefix string = ",\"action\":" + out.RawString(prefix) + out.String(string(in.Action)) + } + { + const prefix string = ",\"sessionid\":" + out.RawString(prefix) + out.String(string(in.SessionId)) + } + if in.UserId != "" { + const prefix string = ",\"userid\":" + out.RawString(prefix) + out.String(string(in.UserId)) + } + if len(in.User) != 0 { + const prefix string = ",\"user\":" + out.RawString(prefix) + out.Raw((in.User).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientSessionRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling22(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientSessionRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling22(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientSessionRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling22(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientSessionRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling22(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling23(in *jlexer.Lexer, out *BackendClientRoomResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "version": + out.Version = string(in.String()) + case "roomid": + out.RoomId = string(in.String()) + case "properties": + if data := in.Raw(); in.Ok() { + in.AddError((out.Properties).UnmarshalJSON(data)) + } + case "session": + if data := in.Raw(); in.Ok() { + in.AddError((out.Session).UnmarshalJSON(data)) + } + case "permissions": + if in.IsNull() { + in.Skip() + out.Permissions = nil + } else { + if out.Permissions == nil { + out.Permissions = new([]Permission) + } + if in.IsNull() { + in.Skip() + *out.Permissions = nil + } else { + in.Delim('[') + if *out.Permissions == nil { + if !in.IsDelim(']') { + *out.Permissions = make([]Permission, 0, 4) + } else { + *out.Permissions = []Permission{} + } + } else { + *out.Permissions = (*out.Permissions)[:0] + } + for !in.IsDelim(']') { + var v50 Permission + v50 = Permission(in.String()) + *out.Permissions = append(*out.Permissions, v50) + in.WantComma() + } + in.Delim(']') + } + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling23(out *jwriter.Writer, in BackendClientRoomResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"version\":" + out.RawString(prefix[1:]) + out.String(string(in.Version)) + } + { + const prefix string = ",\"roomid\":" + out.RawString(prefix) + out.String(string(in.RoomId)) + } + { + const prefix string = ",\"properties\":" + out.RawString(prefix) + out.Raw((in.Properties).MarshalJSON()) + } + if len(in.Session) != 0 { + const prefix string = ",\"session\":" + out.RawString(prefix) + out.Raw((in.Session).MarshalJSON()) + } + if in.Permissions != nil { + const prefix string = ",\"permissions\":" + out.RawString(prefix) + if *in.Permissions == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v51, v52 := range *in.Permissions { + if v51 > 0 { + out.RawByte(',') + } + out.String(string(v52)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientRoomResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling23(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientRoomResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling23(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientRoomResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling23(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientRoomResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling23(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling24(in *jlexer.Lexer, out *BackendClientRoomRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "version": + out.Version = string(in.String()) + case "roomid": + out.RoomId = string(in.String()) + case "action": + out.Action = string(in.String()) + case "userid": + out.UserId = string(in.String()) + case "sessionid": + out.SessionId = string(in.String()) + case "actorid": + out.ActorId = string(in.String()) + case "actortype": + out.ActorType = string(in.String()) + case "incall": + out.InCall = int(in.Int()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling24(out *jwriter.Writer, in BackendClientRoomRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"version\":" + out.RawString(prefix[1:]) + out.String(string(in.Version)) + } + { + const prefix string = ",\"roomid\":" + out.RawString(prefix) + out.String(string(in.RoomId)) + } + if in.Action != "" { + const prefix string = ",\"action\":" + out.RawString(prefix) + out.String(string(in.Action)) + } + { + const prefix string = ",\"userid\":" + out.RawString(prefix) + out.String(string(in.UserId)) + } + { + const prefix string = ",\"sessionid\":" + out.RawString(prefix) + out.String(string(in.SessionId)) + } + if in.ActorId != "" { + const prefix string = ",\"actorid\":" + out.RawString(prefix) + out.String(string(in.ActorId)) + } + if in.ActorType != "" { + const prefix string = ",\"actortype\":" + out.RawString(prefix) + out.String(string(in.ActorType)) + } + if in.InCall != 0 { + const prefix string = ",\"incall\":" + out.RawString(prefix) + out.Int(int(in.InCall)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientRoomRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling24(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientRoomRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling24(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientRoomRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling24(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientRoomRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling24(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling25(in *jlexer.Lexer, out *BackendClientRingResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "version": + out.Version = string(in.String()) + case "roomid": + out.RoomId = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling25(out *jwriter.Writer, in BackendClientRingResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"version\":" + out.RawString(prefix[1:]) + out.String(string(in.Version)) + } + { + const prefix string = ",\"roomid\":" + out.RawString(prefix) + out.String(string(in.RoomId)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientRingResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling25(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientRingResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling25(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientRingResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling25(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientRingResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling25(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling26(in *jlexer.Lexer, out *BackendClientResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "error": + if in.IsNull() { + in.Skip() + out.Error = nil + } else { + if out.Error == nil { + out.Error = new(Error) + } + (*out.Error).UnmarshalEasyJSON(in) + } + case "auth": + if in.IsNull() { + in.Skip() + out.Auth = nil + } else { + if out.Auth == nil { + out.Auth = new(BackendClientAuthResponse) + } + (*out.Auth).UnmarshalEasyJSON(in) + } + case "room": + if in.IsNull() { + in.Skip() + out.Room = nil + } else { + if out.Room == nil { + out.Room = new(BackendClientRoomResponse) + } + (*out.Room).UnmarshalEasyJSON(in) + } + case "ping": + if in.IsNull() { + in.Skip() + out.Ping = nil + } else { + if out.Ping == nil { + out.Ping = new(BackendClientRingResponse) + } + (*out.Ping).UnmarshalEasyJSON(in) + } + case "session": + if in.IsNull() { + in.Skip() + out.Session = nil + } else { + if out.Session == nil { + out.Session = new(BackendClientSessionResponse) + } + (*out.Session).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling26(out *jwriter.Writer, in BackendClientResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + if in.Error != nil { + const prefix string = ",\"error\":" + out.RawString(prefix) + (*in.Error).MarshalEasyJSON(out) + } + if in.Auth != nil { + const prefix string = ",\"auth\":" + out.RawString(prefix) + (*in.Auth).MarshalEasyJSON(out) + } + if in.Room != nil { + const prefix string = ",\"room\":" + out.RawString(prefix) + (*in.Room).MarshalEasyJSON(out) + } + if in.Ping != nil { + const prefix string = ",\"ping\":" + out.RawString(prefix) + (*in.Ping).MarshalEasyJSON(out) + } + if in.Session != nil { + const prefix string = ",\"session\":" + out.RawString(prefix) + (*in.Session).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling26(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling26(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling26(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling26(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling27(in *jlexer.Lexer, out *BackendClientRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "auth": + if in.IsNull() { + in.Skip() + out.Auth = nil + } else { + if out.Auth == nil { + out.Auth = new(BackendClientAuthRequest) + } + (*out.Auth).UnmarshalEasyJSON(in) + } + case "room": + if in.IsNull() { + in.Skip() + out.Room = nil + } else { + if out.Room == nil { + out.Room = new(BackendClientRoomRequest) + } + (*out.Room).UnmarshalEasyJSON(in) + } + case "ping": + if in.IsNull() { + in.Skip() + out.Ping = nil + } else { + if out.Ping == nil { + out.Ping = new(BackendClientPingRequest) + } + (*out.Ping).UnmarshalEasyJSON(in) + } + case "session": + if in.IsNull() { + in.Skip() + out.Session = nil + } else { + if out.Session == nil { + out.Session = new(BackendClientSessionRequest) + } + (*out.Session).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling27(out *jwriter.Writer, in BackendClientRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + if in.Auth != nil { + const prefix string = ",\"auth\":" + out.RawString(prefix) + (*in.Auth).MarshalEasyJSON(out) + } + if in.Room != nil { + const prefix string = ",\"room\":" + out.RawString(prefix) + (*in.Room).MarshalEasyJSON(out) + } + if in.Ping != nil { + const prefix string = ",\"ping\":" + out.RawString(prefix) + (*in.Ping).MarshalEasyJSON(out) + } + if in.Session != nil { + const prefix string = ",\"session\":" + out.RawString(prefix) + (*in.Session).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling27(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling27(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling27(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling27(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling28(in *jlexer.Lexer, out *BackendClientPingRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "version": + out.Version = string(in.String()) + case "roomid": + out.RoomId = string(in.String()) + case "entries": + if in.IsNull() { + in.Skip() + out.Entries = nil + } else { + in.Delim('[') + if out.Entries == nil { + if !in.IsDelim(']') { + out.Entries = make([]BackendPingEntry, 0, 2) + } else { + out.Entries = []BackendPingEntry{} + } + } else { + out.Entries = (out.Entries)[:0] + } + for !in.IsDelim(']') { + var v53 BackendPingEntry + (v53).UnmarshalEasyJSON(in) + out.Entries = append(out.Entries, v53) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling28(out *jwriter.Writer, in BackendClientPingRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"version\":" + out.RawString(prefix[1:]) + out.String(string(in.Version)) + } + { + const prefix string = ",\"roomid\":" + out.RawString(prefix) + out.String(string(in.RoomId)) + } + { + const prefix string = ",\"entries\":" + out.RawString(prefix) + if in.Entries == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v54, v55 := range in.Entries { + if v54 > 0 { + out.RawByte(',') + } + (v55).MarshalEasyJSON(out) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientPingRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling28(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientPingRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling28(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientPingRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling28(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientPingRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling28(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling29(in *jlexer.Lexer, out *BackendClientAuthResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "version": + out.Version = string(in.String()) + case "userid": + out.UserId = string(in.String()) + case "user": + if data := in.Raw(); in.Ok() { + in.AddError((out.User).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling29(out *jwriter.Writer, in BackendClientAuthResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"version\":" + out.RawString(prefix[1:]) + out.String(string(in.Version)) + } + { + const prefix string = ",\"userid\":" + out.RawString(prefix) + out.String(string(in.UserId)) + } + { + const prefix string = ",\"user\":" + out.RawString(prefix) + out.Raw((in.User).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientAuthResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling29(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientAuthResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling29(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientAuthResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling29(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientAuthResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling29(l, v) +} +func easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling30(in *jlexer.Lexer, out *BackendClientAuthRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "version": + out.Version = string(in.String()) + case "params": + if data := in.Raw(); in.Ok() { + in.AddError((out.Params).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling30(out *jwriter.Writer, in BackendClientAuthRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"version\":" + out.RawString(prefix[1:]) + out.String(string(in.Version)) + } + { + const prefix string = ",\"params\":" + out.RawString(prefix) + out.Raw((in.Params).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BackendClientAuthRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling30(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BackendClientAuthRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4354c623EncodeGithubComStrukturagNextcloudSpreedSignaling30(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BackendClientAuthRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling30(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BackendClientAuthRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4354c623DecodeGithubComStrukturagNextcloudSpreedSignaling30(l, v) +} diff --git a/talk/api_test.go b/api_backend_test.go similarity index 95% rename from talk/api_test.go rename to api_backend_test.go index 01c4510..724075d 100644 --- a/talk/api_test.go +++ b/api_backend_test.go @@ -19,21 +19,19 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "net/http" "testing" "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" ) func TestBackendChecksum(t *testing.T) { t.Parallel() assert := assert.New(t) - rnd := internal.RandomString(32) + rnd := newRandomString(32) body := []byte{1, 2, 3, 4, 5} secret := []byte("shared-secret") diff --git a/grpc/api.go b/api_grpc.go similarity index 87% rename from grpc/api.go rename to api_grpc.go index e29df35..127e880 100644 --- a/grpc/api.go +++ b/api_grpc.go @@ -19,21 +19,21 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package grpc +package signaling import ( - "errors" + "fmt" ) // Information on a GRPC target in the etcd cluster. -type TargetInformationEtcd struct { +type GrpcTargetInformationEtcd struct { Address string `json:"address"` } -func (p *TargetInformationEtcd) CheckValid() error { +func (p *GrpcTargetInformationEtcd) CheckValid() error { if l := len(p.Address); l == 0 { - return errors.New("address missing") + return fmt.Errorf("address missing") } else if p.Address[l-1] == '/' { p.Address = p.Address[:l-1] } diff --git a/grpc/api_easyjson.go b/api_grpc_easyjson.go similarity index 56% rename from grpc/api_easyjson.go rename to api_grpc_easyjson.go index f61268c..d1678a4 100644 --- a/grpc/api_easyjson.go +++ b/api_grpc_easyjson.go @@ -1,6 +1,6 @@ // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. -package grpc +package signaling import ( json "encoding/json" @@ -17,7 +17,7 @@ var ( _ easyjson.Marshaler ) -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(in *jlexer.Lexer, out *TargetInformationEtcd) { +func easyjson5dc3c167DecodeGithubComStrukturagNextcloudSpreedSignaling(in *jlexer.Lexer, out *GrpcTargetInformationEtcd) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -30,13 +30,14 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "address": - if in.IsNull() { - in.Skip() - } else { - out.Address = string(in.String()) - } + out.Address = string(in.String()) default: in.SkipRecursive() } @@ -47,7 +48,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(in in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(out *jwriter.Writer, in TargetInformationEtcd) { +func easyjson5dc3c167EncodeGithubComStrukturagNextcloudSpreedSignaling(out *jwriter.Writer, in GrpcTargetInformationEtcd) { out.RawByte('{') first := true _ = first @@ -60,25 +61,25 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(out } // MarshalJSON supports json.Marshaler interface -func (v TargetInformationEtcd) MarshalJSON() ([]byte, error) { +func (v GrpcTargetInformationEtcd) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(&w, v) + easyjson5dc3c167EncodeGithubComStrukturagNextcloudSpreedSignaling(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v TargetInformationEtcd) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(w, v) +func (v GrpcTargetInformationEtcd) MarshalEasyJSON(w *jwriter.Writer) { + easyjson5dc3c167EncodeGithubComStrukturagNextcloudSpreedSignaling(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *TargetInformationEtcd) UnmarshalJSON(data []byte) error { +func (v *GrpcTargetInformationEtcd) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(&r, v) + easyjson5dc3c167DecodeGithubComStrukturagNextcloudSpreedSignaling(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *TargetInformationEtcd) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Grpc(l, v) +func (v *GrpcTargetInformationEtcd) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson5dc3c167DecodeGithubComStrukturagNextcloudSpreedSignaling(l, v) } diff --git a/proxy/api.go b/api_proxy.go similarity index 54% rename from proxy/api.go rename to api_proxy.go index 3663460..23acf35 100644 --- a/proxy/api.go +++ b/api_proxy.go @@ -19,21 +19,17 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package proxy +package signaling import ( "encoding/json" - "errors" "fmt" "net/url" "github.com/golang-jwt/jwt/v5" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" ) -type ClientMessage struct { +type ProxyClientMessage struct { json.Marshaler json.Unmarshaler @@ -44,16 +40,16 @@ type ClientMessage struct { Type string `json:"type"` // Filled for type "hello" - Hello *HelloClientMessage `json:"hello,omitempty"` + Hello *HelloProxyClientMessage `json:"hello,omitempty"` - Bye *ByeClientMessage `json:"bye,omitempty"` + Bye *ByeProxyClientMessage `json:"bye,omitempty"` - Command *CommandClientMessage `json:"command,omitempty"` + Command *CommandProxyClientMessage `json:"command,omitempty"` - Payload *PayloadClientMessage `json:"payload,omitempty"` + Payload *PayloadProxyClientMessage `json:"payload,omitempty"` } -func (m *ClientMessage) String() string { +func (m *ProxyClientMessage) String() string { data, err := json.Marshal(m) if err != nil { return fmt.Sprintf("Could not serialize %#v: %s", m, err) @@ -61,13 +57,13 @@ func (m *ClientMessage) String() string { return string(data) } -func (m *ClientMessage) CheckValid() error { +func (m *ProxyClientMessage) CheckValid() error { switch m.Type { case "": - return errors.New("type missing") + return fmt.Errorf("type missing") case "hello": if m.Hello == nil { - return errors.New("hello missing") + return fmt.Errorf("hello missing") } else if err := m.Hello.CheckValid(); err != nil { return err } @@ -80,13 +76,13 @@ func (m *ClientMessage) CheckValid() error { } case "command": if m.Command == nil { - return errors.New("command missing") + return fmt.Errorf("command missing") } else if err := m.Command.CheckValid(); err != nil { return err } case "payload": if m.Payload == nil { - return errors.New("payload missing") + return fmt.Errorf("payload missing") } else if err := m.Payload.CheckValid(); err != nil { return err } @@ -94,20 +90,20 @@ func (m *ClientMessage) CheckValid() error { return nil } -func (m *ClientMessage) NewErrorServerMessage(e *api.Error) *ServerMessage { - return &ServerMessage{ +func (m *ProxyClientMessage) NewErrorServerMessage(e *Error) *ProxyServerMessage { + return &ProxyServerMessage{ Id: m.Id, Type: "error", Error: e, } } -func (m *ClientMessage) NewWrappedErrorServerMessage(e error) *ServerMessage { - return m.NewErrorServerMessage(api.NewError("internal_error", e.Error())) +func (m *ProxyClientMessage) NewWrappedErrorServerMessage(e error) *ProxyServerMessage { + return m.NewErrorServerMessage(NewError("internal_error", e.Error())) } -// ServerMessage is a message that is sent from the server to a client. -type ServerMessage struct { +// ProxyServerMessage is a message that is sent from the server to a client. +type ProxyServerMessage struct { json.Marshaler json.Unmarshaler @@ -115,20 +111,20 @@ type ServerMessage struct { Type string `json:"type"` - Error *api.Error `json:"error,omitempty"` + Error *Error `json:"error,omitempty"` - Hello *HelloServerMessage `json:"hello,omitempty"` + Hello *HelloProxyServerMessage `json:"hello,omitempty"` - Bye *ByeServerMessage `json:"bye,omitempty"` + Bye *ByeProxyServerMessage `json:"bye,omitempty"` - Command *CommandServerMessage `json:"command,omitempty"` + Command *CommandProxyServerMessage `json:"command,omitempty"` - Payload *PayloadServerMessage `json:"payload,omitempty"` + Payload *PayloadProxyServerMessage `json:"payload,omitempty"` - Event *EventServerMessage `json:"event,omitempty"` + Event *EventProxyServerMessage `json:"event,omitempty"` } -func (r *ServerMessage) String() string { +func (r *ProxyServerMessage) String() string { data, err := json.Marshal(r) if err != nil { return fmt.Sprintf("Could not serialize %#v: %s", r, err) @@ -136,7 +132,7 @@ func (r *ServerMessage) String() string { return string(data) } -func (r *ServerMessage) CloseAfterSend(session api.RoomAware) bool { +func (r *ProxyServerMessage) CloseAfterSend(session Session) bool { switch r.Type { case "bye": return true @@ -151,10 +147,10 @@ type TokenClaims struct { jwt.RegisteredClaims } -type HelloClientMessage struct { +type HelloProxyClientMessage struct { Version string `json:"version"` - ResumeId api.PublicSessionId `json:"resumeid"` + ResumeId string `json:"resumeid"` Features []string `json:"features,omitempty"` @@ -162,55 +158,65 @@ type HelloClientMessage struct { Token string `json:"token"` } -func (m *HelloClientMessage) CheckValid() error { - if m.Version != api.HelloVersionV1 { +func (m *HelloProxyClientMessage) CheckValid() error { + if m.Version != HelloVersionV1 { return fmt.Errorf("unsupported hello version: %s", m.Version) } if m.ResumeId == "" { if m.Token == "" { - return errors.New("token missing") + return fmt.Errorf("token missing") } } return nil } -type HelloServerMessage struct { +type HelloProxyServerMessage struct { Version string `json:"version"` - SessionId api.PublicSessionId `json:"sessionid"` - Server *api.WelcomeServerMessage `json:"server,omitempty"` + SessionId string `json:"sessionid"` + Server *WelcomeServerMessage `json:"server,omitempty"` } // Type "bye" -type ByeClientMessage struct { +type ByeProxyClientMessage struct { } -func (m *ByeClientMessage) CheckValid() error { +func (m *ByeProxyClientMessage) CheckValid() error { // No additional validation required. return nil } -type ByeServerMessage struct { +type ByeProxyServerMessage struct { Reason string `json:"reason"` } // Type "command" -type CommandClientMessage struct { +type NewPublisherSettings struct { + Bitrate int `json:"bitrate,omitempty"` + MediaTypes MediaType `json:"mediatypes,omitempty"` + + AudioCodec string `json:"audiocodec,omitempty"` + VideoCodec string `json:"videocodec,omitempty"` + VP9Profile string `json:"vp9_profile,omitempty"` + H264Profile string `json:"h264_profile,omitempty"` +} + +type CommandProxyClientMessage struct { Type string `json:"type"` - Sid string `json:"sid,omitempty"` - StreamType sfu.StreamType `json:"streamType,omitempty"` - PublisherId api.PublicSessionId `json:"publisherId,omitempty"` - ClientId string `json:"clientId,omitempty"` + Sid string `json:"sid,omitempty"` + StreamType StreamType `json:"streamType,omitempty"` + PublisherId string `json:"publisherId,omitempty"` + ClientId string `json:"clientId,omitempty"` // Deprecated: use PublisherSettings instead. - Bitrate api.Bandwidth `json:"bitrate,omitempty"` + Bitrate int `json:"bitrate,omitempty"` // Deprecated: use PublisherSettings instead. - MediaTypes sfu.MediaType `json:"mediatypes,omitempty"` + MediaTypes MediaType `json:"mediatypes,omitempty"` - PublisherSettings *sfu.NewPublisherSettings `json:"publisherSettings,omitempty"` + PublisherSettings *NewPublisherSettings `json:"publisherSettings,omitempty"` RemoteUrl string `json:"remoteUrl,omitempty"` remoteUrl *url.URL @@ -221,24 +227,24 @@ type CommandClientMessage struct { RtcpPort int `json:"rtcpPort,omitempty"` } -func (m *CommandClientMessage) CheckValid() error { +func (m *CommandProxyClientMessage) CheckValid() error { switch m.Type { case "": - return errors.New("type missing") + return fmt.Errorf("type missing") case "create-publisher": if m.StreamType == "" { - return errors.New("stream type missing") + return fmt.Errorf("stream type missing") } case "create-subscriber": if m.PublisherId == "" { - return errors.New("publisher id missing") + return fmt.Errorf("publisher id missing") } if m.StreamType == "" { - return errors.New("stream type missing") + return fmt.Errorf("stream type missing") } if m.RemoteUrl != "" { if m.RemoteToken == "" { - return errors.New("remote token missing") + return fmt.Errorf("remote token missing") } remoteUrl, err := url.Parse(m.RemoteUrl) @@ -251,42 +257,42 @@ func (m *CommandClientMessage) CheckValid() error { fallthrough case "delete-subscriber": if m.ClientId == "" { - return errors.New("client id missing") + return fmt.Errorf("client id missing") } } return nil } -type CommandServerMessage struct { +type CommandProxyServerMessage struct { Id string `json:"id,omitempty"` Sid string `json:"sid,omitempty"` - Bitrate api.Bandwidth `json:"bitrate,omitempty"` + Bitrate int `json:"bitrate,omitempty"` - Streams []sfu.PublisherStream `json:"streams,omitempty"` + Streams []PublisherStream `json:"streams,omitempty"` } // Type "payload" -type PayloadClientMessage struct { +type PayloadProxyClientMessage struct { Type string `json:"type"` - ClientId string `json:"clientId"` - Sid string `json:"sid,omitempty"` - Payload api.StringMap `json:"payload,omitempty"` + ClientId string `json:"clientId"` + Sid string `json:"sid,omitempty"` + Payload map[string]interface{} `json:"payload,omitempty"` } -func (m *PayloadClientMessage) CheckValid() error { +func (m *PayloadProxyClientMessage) CheckValid() error { switch m.Type { case "": - return errors.New("type missing") + return fmt.Errorf("type missing") case "offer": fallthrough case "answer": fallthrough case "candidate": if len(m.Payload) == 0 { - return errors.New("payload missing") + return fmt.Errorf("payload missing") } case "endOfCandidates": fallthrough @@ -294,72 +300,66 @@ func (m *PayloadClientMessage) CheckValid() error { // No payload required. } if m.ClientId == "" { - return errors.New("client id missing") + return fmt.Errorf("client id missing") } return nil } -type PayloadServerMessage struct { +type PayloadProxyServerMessage struct { Type string `json:"type"` - ClientId string `json:"clientId"` - Payload api.StringMap `json:"payload"` + ClientId string `json:"clientId"` + Payload map[string]interface{} `json:"payload"` } // Type "event" -type EventServerBandwidth struct { +type EventProxyServerBandwidth struct { // Incoming is the bandwidth utilization for publishers in percent. Incoming *float64 `json:"incoming,omitempty"` // Outgoing is the bandwidth utilization for subscribers in percent. Outgoing *float64 `json:"outgoing,omitempty"` - - // Received is the incoming bandwidth. - Received api.Bandwidth `json:"received,omitempty"` - // Sent is the outgoing bandwidth. - Sent api.Bandwidth `json:"sent,omitempty"` } -func (b *EventServerBandwidth) String() string { - switch { - case b.Incoming != nil && b.Outgoing != nil: +func (b *EventProxyServerBandwidth) String() string { + if b.Incoming != nil && b.Outgoing != nil { return fmt.Sprintf("bandwidth: incoming=%.3f%%, outgoing=%.3f%%", *b.Incoming, *b.Outgoing) - case b.Incoming != nil: + } else if b.Incoming != nil { return fmt.Sprintf("bandwidth: incoming=%.3f%%, outgoing=unlimited", *b.Incoming) - case b.Outgoing != nil: + } else if b.Outgoing != nil { return fmt.Sprintf("bandwidth: incoming=unlimited, outgoing=%.3f%%", *b.Outgoing) - default: + } else { return "bandwidth: incoming=unlimited, outgoing=unlimited" } } -func (b EventServerBandwidth) AllowIncoming() bool { +func (b EventProxyServerBandwidth) AllowIncoming() bool { return b.Incoming == nil || *b.Incoming < 100 } -func (b EventServerBandwidth) AllowOutgoing() bool { +func (b EventProxyServerBandwidth) AllowOutgoing() bool { return b.Outgoing == nil || *b.Outgoing < 100 } -type EventServerMessage struct { +type EventProxyServerMessage struct { Type string `json:"type"` ClientId string `json:"clientId,omitempty"` - Load uint64 `json:"load,omitempty"` + Load int64 `json:"load,omitempty"` Sid string `json:"sid,omitempty"` - Bandwidth *EventServerBandwidth `json:"bandwidth,omitempty"` + Bandwidth *EventProxyServerBandwidth `json:"bandwidth,omitempty"` } // Information on a proxy in the etcd cluster. -type InformationEtcd struct { +type ProxyInformationEtcd struct { Address string `json:"address"` } -func (p *InformationEtcd) CheckValid() error { +func (p *ProxyInformationEtcd) CheckValid() error { if p.Address == "" { - return errors.New("address missing") + return fmt.Errorf("address missing") } if p.Address[len(p.Address)-1] != '/' { p.Address += "/" diff --git a/proxy/api_easyjson.go b/api_proxy_easyjson.go similarity index 58% rename from proxy/api_easyjson.go rename to api_proxy_easyjson.go index 163a495..e41b81c 100644 --- a/proxy/api_easyjson.go +++ b/api_proxy_easyjson.go @@ -1,6 +1,6 @@ // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. -package proxy +package signaling import ( json "encoding/json" @@ -8,8 +8,6 @@ import ( easyjson "github.com/mailru/easyjson" jlexer "github.com/mailru/easyjson/jlexer" jwriter "github.com/mailru/easyjson/jwriter" - api "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - sfu "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" ) // suppress unused package warning @@ -20,7 +18,7 @@ var ( _ easyjson.Marshaler ) -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(in *jlexer.Lexer, out *TokenClaims) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling(in *jlexer.Lexer, out *TokenClaims) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -33,26 +31,19 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "iss": - if in.IsNull() { - in.Skip() - } else { - out.Issuer = string(in.String()) - } + out.Issuer = string(in.String()) case "sub": - if in.IsNull() { - in.Skip() - } else { - out.Subject = string(in.String()) - } + out.Subject = string(in.String()) case "aud": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Audience).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Audience).UnmarshalJSON(data)) } case "exp": if in.IsNull() { @@ -62,12 +53,8 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(in if out.ExpiresAt == nil { out.ExpiresAt = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.ExpiresAt).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.ExpiresAt).UnmarshalJSON(data)) } } case "nbf": @@ -78,12 +65,8 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(in if out.NotBefore == nil { out.NotBefore = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.NotBefore).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.NotBefore).UnmarshalJSON(data)) } } case "iat": @@ -94,20 +77,12 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(in if out.IssuedAt == nil { out.IssuedAt = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.IssuedAt).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.IssuedAt).UnmarshalJSON(data)) } } case "jti": - if in.IsNull() { - in.Skip() - } else { - out.ID = string(in.String()) - } + out.ID = string(in.String()) default: in.SkipRecursive() } @@ -118,7 +93,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(in in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(out *jwriter.Writer, in TokenClaims) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling(out *jwriter.Writer, in TokenClaims) { out.RawByte('{') first := true _ = first @@ -194,27 +169,27 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(ou // MarshalJSON supports json.Marshaler interface func (v TokenClaims) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v TokenClaims) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *TokenClaims) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *TokenClaims) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy(l, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(in *jlexer.Lexer, out *ServerMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling1(in *jlexer.Lexer, out *ProxyServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -227,32 +202,25 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "id": - if in.IsNull() { - in.Skip() - } else { - out.Id = string(in.String()) - } + out.Id = string(in.String()) case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "error": if in.IsNull() { in.Skip() out.Error = nil } else { if out.Error == nil { - out.Error = new(api.Error) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Error).UnmarshalEasyJSON(in) + out.Error = new(Error) } + (*out.Error).UnmarshalEasyJSON(in) } case "hello": if in.IsNull() { @@ -260,13 +228,9 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(i out.Hello = nil } else { if out.Hello == nil { - out.Hello = new(HelloServerMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Hello).UnmarshalEasyJSON(in) + out.Hello = new(HelloProxyServerMessage) } + (*out.Hello).UnmarshalEasyJSON(in) } case "bye": if in.IsNull() { @@ -274,13 +238,9 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(i out.Bye = nil } else { if out.Bye == nil { - out.Bye = new(ByeServerMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Bye).UnmarshalEasyJSON(in) + out.Bye = new(ByeProxyServerMessage) } + (*out.Bye).UnmarshalEasyJSON(in) } case "command": if in.IsNull() { @@ -288,13 +248,9 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(i out.Command = nil } else { if out.Command == nil { - out.Command = new(CommandServerMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Command).UnmarshalEasyJSON(in) + out.Command = new(CommandProxyServerMessage) } + (*out.Command).UnmarshalEasyJSON(in) } case "payload": if in.IsNull() { @@ -302,13 +258,9 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(i out.Payload = nil } else { if out.Payload == nil { - out.Payload = new(PayloadServerMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Payload).UnmarshalEasyJSON(in) + out.Payload = new(PayloadProxyServerMessage) } + (*out.Payload).UnmarshalEasyJSON(in) } case "event": if in.IsNull() { @@ -316,13 +268,9 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(i out.Event = nil } else { if out.Event == nil { - out.Event = new(EventServerMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Event).UnmarshalEasyJSON(in) + out.Event = new(EventProxyServerMessage) } + (*out.Event).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -334,7 +282,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(out *jwriter.Writer, in ServerMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling1(out *jwriter.Writer, in ProxyServerMessage) { out.RawByte('{') first := true _ = first @@ -388,29 +336,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(o } // MarshalJSON supports json.Marshaler interface -func (v ServerMessage) MarshalJSON() ([]byte, error) { +func (v ProxyServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling1(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v ServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(w, v) +func (v ProxyServerMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling1(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *ServerMessage) UnmarshalJSON(data []byte) error { +func (v *ProxyServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling1(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *ServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy1(l, v) +func (v *ProxyServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling1(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(in *jlexer.Lexer, out *PayloadServerMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlexer.Lexer, out *ProxyInformationEtcd) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -423,25 +371,227 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "address": + out.Address = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling2(out *jwriter.Writer, in ProxyInformationEtcd) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"address\":" + out.RawString(prefix[1:]) + out.String(string(in.Address)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ProxyInformationEtcd) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ProxyInformationEtcd) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ProxyInformationEtcd) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ProxyInformationEtcd) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling2(l, v) +} +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling3(in *jlexer.Lexer, out *ProxyClientMessage) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.Id = string(in.String()) + case "type": + out.Type = string(in.String()) + case "hello": + if in.IsNull() { + in.Skip() + out.Hello = nil + } else { + if out.Hello == nil { + out.Hello = new(HelloProxyClientMessage) + } + (*out.Hello).UnmarshalEasyJSON(in) + } + case "bye": + if in.IsNull() { + in.Skip() + out.Bye = nil + } else { + if out.Bye == nil { + out.Bye = new(ByeProxyClientMessage) + } + (*out.Bye).UnmarshalEasyJSON(in) + } + case "command": + if in.IsNull() { + in.Skip() + out.Command = nil + } else { + if out.Command == nil { + out.Command = new(CommandProxyClientMessage) + } + (*out.Command).UnmarshalEasyJSON(in) + } + case "payload": + if in.IsNull() { + in.Skip() + out.Payload = nil + } else { + if out.Payload == nil { + out.Payload = new(PayloadProxyClientMessage) + } + (*out.Payload).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling3(out *jwriter.Writer, in ProxyClientMessage) { + out.RawByte('{') + first := true + _ = first + if in.Id != "" { + const prefix string = ",\"id\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.Id)) + } + { + const prefix string = ",\"type\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Type)) + } + if in.Hello != nil { + const prefix string = ",\"hello\":" + out.RawString(prefix) + (*in.Hello).MarshalEasyJSON(out) + } + if in.Bye != nil { + const prefix string = ",\"bye\":" + out.RawString(prefix) + (*in.Bye).MarshalEasyJSON(out) + } + if in.Command != nil { + const prefix string = ",\"command\":" + out.RawString(prefix) + (*in.Command).MarshalEasyJSON(out) + } + if in.Payload != nil { + const prefix string = ",\"payload\":" + out.RawString(prefix) + (*in.Payload).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ProxyClientMessage) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ProxyClientMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ProxyClientMessage) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ProxyClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling3(l, v) +} +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling4(in *jlexer.Lexer, out *PayloadProxyServerMessage) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "clientId": - if in.IsNull() { - in.Skip() - } else { - out.ClientId = string(in.String()) - } + out.ClientId = string(in.String()) case "payload": if in.IsNull() { in.Skip() } else { in.Delim('{') - out.Payload = make(api.StringMap) + out.Payload = make(map[string]interface{}) for !in.IsDelim('}') { key := string(in.String()) in.WantColon() @@ -468,7 +618,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(out *jwriter.Writer, in PayloadServerMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling4(out *jwriter.Writer, in PayloadProxyServerMessage) { out.RawByte('{') first := true _ = first @@ -513,29 +663,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(o } // MarshalJSON supports json.Marshaler interface -func (v PayloadServerMessage) MarshalJSON() ([]byte, error) { +func (v PayloadProxyServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling4(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v PayloadServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(w, v) +func (v PayloadProxyServerMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling4(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *PayloadServerMessage) UnmarshalJSON(data []byte) error { +func (v *PayloadProxyServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling4(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *PayloadServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy2(l, v) +func (v *PayloadProxyServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling4(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(in *jlexer.Lexer, out *PayloadClientMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling5(in *jlexer.Lexer, out *PayloadProxyClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -548,32 +698,25 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "clientId": - if in.IsNull() { - in.Skip() - } else { - out.ClientId = string(in.String()) - } + out.ClientId = string(in.String()) case "sid": - if in.IsNull() { - in.Skip() - } else { - out.Sid = string(in.String()) - } + out.Sid = string(in.String()) case "payload": if in.IsNull() { in.Skip() } else { in.Delim('{') if !in.IsDelim('}') { - out.Payload = make(api.StringMap) + out.Payload = make(map[string]interface{}) } else { out.Payload = nil } @@ -603,7 +746,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(out *jwriter.Writer, in PayloadClientMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling5(out *jwriter.Writer, in PayloadProxyClientMessage) { out.RawByte('{') first := true _ = first @@ -651,29 +794,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(o } // MarshalJSON supports json.Marshaler interface -func (v PayloadClientMessage) MarshalJSON() ([]byte, error) { +func (v PayloadProxyClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling5(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v PayloadClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(w, v) +func (v PayloadProxyClientMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling5(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *PayloadClientMessage) UnmarshalJSON(data []byte) error { +func (v *PayloadProxyClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling5(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *PayloadClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy3(l, v) +func (v *PayloadProxyClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling5(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy4(in *jlexer.Lexer, out *InformationEtcd) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling6(in *jlexer.Lexer, out *NewPublisherSettings) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -686,13 +829,24 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy4(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { - case "address": - if in.IsNull() { - in.Skip() - } else { - out.Address = string(in.String()) - } + case "bitrate": + out.Bitrate = int(in.Int()) + case "mediatypes": + out.MediaTypes = MediaType(in.Int()) + case "audiocodec": + out.AudioCodec = string(in.String()) + case "videocodec": + out.VideoCodec = string(in.String()) + case "vp9_profile": + out.VP9Profile = string(in.String()) + case "h264_profile": + out.H264Profile = string(in.String()) default: in.SkipRecursive() } @@ -703,42 +857,93 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy4(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy4(out *jwriter.Writer, in InformationEtcd) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling6(out *jwriter.Writer, in NewPublisherSettings) { out.RawByte('{') first := true _ = first - { - const prefix string = ",\"address\":" + if in.Bitrate != 0 { + const prefix string = ",\"bitrate\":" + first = false out.RawString(prefix[1:]) - out.String(string(in.Address)) + out.Int(int(in.Bitrate)) + } + if in.MediaTypes != 0 { + const prefix string = ",\"mediatypes\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int(int(in.MediaTypes)) + } + if in.AudioCodec != "" { + const prefix string = ",\"audiocodec\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.AudioCodec)) + } + if in.VideoCodec != "" { + const prefix string = ",\"videocodec\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.VideoCodec)) + } + if in.VP9Profile != "" { + const prefix string = ",\"vp9_profile\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.VP9Profile)) + } + if in.H264Profile != "" { + const prefix string = ",\"h264_profile\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.H264Profile)) } out.RawByte('}') } // MarshalJSON supports json.Marshaler interface -func (v InformationEtcd) MarshalJSON() ([]byte, error) { +func (v NewPublisherSettings) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy4(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling6(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v InformationEtcd) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy4(w, v) +func (v NewPublisherSettings) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling6(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *InformationEtcd) UnmarshalJSON(data []byte) error { +func (v *NewPublisherSettings) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy4(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling6(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *InformationEtcd) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy4(l, v) +func (v *NewPublisherSettings) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling6(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(in *jlexer.Lexer, out *HelloServerMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling7(in *jlexer.Lexer, out *HelloProxyServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -751,32 +956,25 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } + out.Version = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = api.PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "server": if in.IsNull() { in.Skip() out.Server = nil } else { if out.Server == nil { - out.Server = new(api.WelcomeServerMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Server).UnmarshalEasyJSON(in) + out.Server = new(WelcomeServerMessage) } + (*out.Server).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -788,7 +986,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(out *jwriter.Writer, in HelloServerMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling7(out *jwriter.Writer, in HelloProxyServerMessage) { out.RawByte('{') first := true _ = first @@ -811,29 +1009,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(o } // MarshalJSON supports json.Marshaler interface -func (v HelloServerMessage) MarshalJSON() ([]byte, error) { +func (v HelloProxyServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling7(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v HelloServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(w, v) +func (v HelloProxyServerMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling7(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *HelloServerMessage) UnmarshalJSON(data []byte) error { +func (v *HelloProxyServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling7(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *HelloServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy5(l, v) +func (v *HelloProxyServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling7(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(in *jlexer.Lexer, out *HelloClientMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling8(in *jlexer.Lexer, out *HelloProxyClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -846,19 +1044,16 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } + out.Version = string(in.String()) case "resumeid": - if in.IsNull() { - in.Skip() - } else { - out.ResumeId = api.PublicSessionId(in.String()) - } + out.ResumeId = string(in.String()) case "features": if in.IsNull() { in.Skip() @@ -876,22 +1071,14 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(i } for !in.IsDelim(']') { var v5 string - if in.IsNull() { - in.Skip() - } else { - v5 = string(in.String()) - } + v5 = string(in.String()) out.Features = append(out.Features, v5) in.WantComma() } in.Delim(']') } case "token": - if in.IsNull() { - in.Skip() - } else { - out.Token = string(in.String()) - } + out.Token = string(in.String()) default: in.SkipRecursive() } @@ -902,7 +1089,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(out *jwriter.Writer, in HelloClientMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling8(out *jwriter.Writer, in HelloProxyClientMessage) { out.RawByte('{') first := true _ = first @@ -939,29 +1126,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(o } // MarshalJSON supports json.Marshaler interface -func (v HelloClientMessage) MarshalJSON() ([]byte, error) { +func (v HelloProxyClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling8(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v HelloClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(w, v) +func (v HelloProxyClientMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling8(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *HelloClientMessage) UnmarshalJSON(data []byte) error { +func (v *HelloProxyClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling8(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *HelloClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy6(l, v) +func (v *HelloProxyClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling8(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(in *jlexer.Lexer, out *EventServerMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling9(in *jlexer.Lexer, out *EventProxyServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -974,44 +1161,29 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "clientId": - if in.IsNull() { - in.Skip() - } else { - out.ClientId = string(in.String()) - } + out.ClientId = string(in.String()) case "load": - if in.IsNull() { - in.Skip() - } else { - out.Load = uint64(in.Uint64()) - } + out.Load = int64(in.Int64()) case "sid": - if in.IsNull() { - in.Skip() - } else { - out.Sid = string(in.String()) - } + out.Sid = string(in.String()) case "bandwidth": if in.IsNull() { in.Skip() out.Bandwidth = nil } else { if out.Bandwidth == nil { - out.Bandwidth = new(EventServerBandwidth) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Bandwidth).UnmarshalEasyJSON(in) + out.Bandwidth = new(EventProxyServerBandwidth) } + (*out.Bandwidth).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -1023,7 +1195,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(out *jwriter.Writer, in EventServerMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling9(out *jwriter.Writer, in EventProxyServerMessage) { out.RawByte('{') first := true _ = first @@ -1040,7 +1212,7 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(o if in.Load != 0 { const prefix string = ",\"load\":" out.RawString(prefix) - out.Uint64(uint64(in.Load)) + out.Int64(int64(in.Load)) } if in.Sid != "" { const prefix string = ",\"sid\":" @@ -1056,29 +1228,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(o } // MarshalJSON supports json.Marshaler interface -func (v EventServerMessage) MarshalJSON() ([]byte, error) { +func (v EventProxyServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling9(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v EventServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(w, v) +func (v EventProxyServerMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling9(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *EventServerMessage) UnmarshalJSON(data []byte) error { +func (v *EventProxyServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling9(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *EventServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy7(l, v) +func (v *EventProxyServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling9(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(in *jlexer.Lexer, out *EventServerBandwidth) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling10(in *jlexer.Lexer, out *EventProxyServerBandwidth) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1091,6 +1263,11 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "incoming": if in.IsNull() { @@ -1100,11 +1277,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(i if out.Incoming == nil { out.Incoming = new(float64) } - if in.IsNull() { - in.Skip() - } else { - *out.Incoming = float64(in.Float64()) - } + *out.Incoming = float64(in.Float64()) } case "outgoing": if in.IsNull() { @@ -1114,23 +1287,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(i if out.Outgoing == nil { out.Outgoing = new(float64) } - if in.IsNull() { - in.Skip() - } else { - *out.Outgoing = float64(in.Float64()) - } - } - case "received": - if in.IsNull() { - in.Skip() - } else { - out.Received = api.Bandwidth(in.Uint64()) - } - case "sent": - if in.IsNull() { - in.Skip() - } else { - out.Sent = api.Bandwidth(in.Uint64()) + *out.Outgoing = float64(in.Float64()) } default: in.SkipRecursive() @@ -1142,7 +1299,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(out *jwriter.Writer, in EventServerBandwidth) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling10(out *jwriter.Writer, in EventProxyServerBandwidth) { out.RawByte('{') first := true _ = first @@ -1162,53 +1319,33 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(o } out.Float64(float64(*in.Outgoing)) } - if in.Received != 0 { - const prefix string = ",\"received\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Uint64(uint64(in.Received)) - } - if in.Sent != 0 { - const prefix string = ",\"sent\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Uint64(uint64(in.Sent)) - } out.RawByte('}') } // MarshalJSON supports json.Marshaler interface -func (v EventServerBandwidth) MarshalJSON() ([]byte, error) { +func (v EventProxyServerBandwidth) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling10(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v EventServerBandwidth) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(w, v) +func (v EventProxyServerBandwidth) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling10(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *EventServerBandwidth) UnmarshalJSON(data []byte) error { +func (v *EventProxyServerBandwidth) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling10(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *EventServerBandwidth) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy8(l, v) +func (v *EventProxyServerBandwidth) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling10(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(in *jlexer.Lexer, out *CommandServerMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling11(in *jlexer.Lexer, out *CommandProxyServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1221,25 +1358,18 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(i for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "id": - if in.IsNull() { - in.Skip() - } else { - out.Id = string(in.String()) - } + out.Id = string(in.String()) case "sid": - if in.IsNull() { - in.Skip() - } else { - out.Sid = string(in.String()) - } + out.Sid = string(in.String()) case "bitrate": - if in.IsNull() { - in.Skip() - } else { - out.Bitrate = api.Bandwidth(in.Uint64()) - } + out.Bitrate = int(in.Int()) case "streams": if in.IsNull() { in.Skip() @@ -1248,16 +1378,16 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(i in.Delim('[') if out.Streams == nil { if !in.IsDelim(']') { - out.Streams = make([]sfu.PublisherStream, 0, 0) + out.Streams = make([]PublisherStream, 0, 0) } else { - out.Streams = []sfu.PublisherStream{} + out.Streams = []PublisherStream{} } } else { out.Streams = (out.Streams)[:0] } for !in.IsDelim(']') { - var v8 sfu.PublisherStream - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu(in, &v8) + var v8 PublisherStream + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling12(in, &v8) out.Streams = append(out.Streams, v8) in.WantComma() } @@ -1273,7 +1403,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(i in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(out *jwriter.Writer, in CommandServerMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling11(out *jwriter.Writer, in CommandProxyServerMessage) { out.RawByte('{') first := true _ = first @@ -1301,7 +1431,7 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(o } else { out.RawString(prefix) } - out.Uint64(uint64(in.Bitrate)) + out.Int(int(in.Bitrate)) } if len(in.Streams) != 0 { const prefix string = ",\"streams\":" @@ -1317,7 +1447,7 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(o if v9 > 0 { out.RawByte(',') } - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu(out, v10) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling12(out, v10) } out.RawByte(']') } @@ -1326,29 +1456,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(o } // MarshalJSON supports json.Marshaler interface -func (v CommandServerMessage) MarshalJSON() ([]byte, error) { +func (v CommandProxyServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling11(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v CommandServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(w, v) +func (v CommandProxyServerMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling11(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *CommandServerMessage) UnmarshalJSON(data []byte) error { +func (v *CommandProxyServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling11(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *CommandServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy9(l, v) +func (v *CommandProxyServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling11(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu(in *jlexer.Lexer, out *sfu.PublisherStream) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling12(in *jlexer.Lexer, out *PublisherStream) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1361,97 +1491,42 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "mid": - if in.IsNull() { - in.Skip() - } else { - out.Mid = string(in.String()) - } + out.Mid = string(in.String()) case "mindex": - if in.IsNull() { - in.Skip() - } else { - out.Mindex = int(in.Int()) - } + out.Mindex = int(in.Int()) case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "description": - if in.IsNull() { - in.Skip() - } else { - out.Description = string(in.String()) - } + out.Description = string(in.String()) case "disabled": - if in.IsNull() { - in.Skip() - } else { - out.Disabled = bool(in.Bool()) - } + out.Disabled = bool(in.Bool()) case "codec": - if in.IsNull() { - in.Skip() - } else { - out.Codec = string(in.String()) - } + out.Codec = string(in.String()) case "stereo": - if in.IsNull() { - in.Skip() - } else { - out.Stereo = bool(in.Bool()) - } + out.Stereo = bool(in.Bool()) case "fec": - if in.IsNull() { - in.Skip() - } else { - out.Fec = bool(in.Bool()) - } + out.Fec = bool(in.Bool()) case "dtx": - if in.IsNull() { - in.Skip() - } else { - out.Dtx = bool(in.Bool()) - } + out.Dtx = bool(in.Bool()) case "simulcast": - if in.IsNull() { - in.Skip() - } else { - out.Simulcast = bool(in.Bool()) - } + out.Simulcast = bool(in.Bool()) case "svc": - if in.IsNull() { - in.Skip() - } else { - out.Svc = bool(in.Bool()) - } + out.Svc = bool(in.Bool()) case "h264_profile": - if in.IsNull() { - in.Skip() - } else { - out.ProfileH264 = string(in.String()) - } + out.ProfileH264 = string(in.String()) case "vp9_profile": - if in.IsNull() { - in.Skip() - } else { - out.ProfileVP9 = string(in.String()) - } + out.ProfileVP9 = string(in.String()) case "videoorient_ext_id": - if in.IsNull() { - in.Skip() - } else { - out.ExtIdVideoOrientation = int(in.Int()) - } + out.ExtIdVideoOrientation = int(in.Int()) case "playoutdelay_ext_id": - if in.IsNull() { - in.Skip() - } else { - out.ExtIdPlayoutDelay = int(in.Int()) - } + out.ExtIdPlayoutDelay = int(in.Int()) default: in.SkipRecursive() } @@ -1462,7 +1537,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu(in * in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu(out *jwriter.Writer, in sfu.PublisherStream) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling12(out *jwriter.Writer, in PublisherStream) { out.RawByte('{') first := true _ = first @@ -1543,7 +1618,7 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu(out } out.RawByte('}') } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10(in *jlexer.Lexer, out *CommandClientMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling13(in *jlexer.Lexer, out *CommandProxyClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1556,89 +1631,46 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10( for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "sid": - if in.IsNull() { - in.Skip() - } else { - out.Sid = string(in.String()) - } + out.Sid = string(in.String()) case "streamType": - if in.IsNull() { - in.Skip() - } else { - out.StreamType = sfu.StreamType(in.String()) - } + out.StreamType = StreamType(in.String()) case "publisherId": - if in.IsNull() { - in.Skip() - } else { - out.PublisherId = api.PublicSessionId(in.String()) - } + out.PublisherId = string(in.String()) case "clientId": - if in.IsNull() { - in.Skip() - } else { - out.ClientId = string(in.String()) - } + out.ClientId = string(in.String()) case "bitrate": - if in.IsNull() { - in.Skip() - } else { - out.Bitrate = api.Bandwidth(in.Uint64()) - } + out.Bitrate = int(in.Int()) case "mediatypes": - if in.IsNull() { - in.Skip() - } else { - out.MediaTypes = sfu.MediaType(in.Int()) - } + out.MediaTypes = MediaType(in.Int()) case "publisherSettings": if in.IsNull() { in.Skip() out.PublisherSettings = nil } else { if out.PublisherSettings == nil { - out.PublisherSettings = new(sfu.NewPublisherSettings) + out.PublisherSettings = new(NewPublisherSettings) } - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu1(in, out.PublisherSettings) + (*out.PublisherSettings).UnmarshalEasyJSON(in) } case "remoteUrl": - if in.IsNull() { - in.Skip() - } else { - out.RemoteUrl = string(in.String()) - } + out.RemoteUrl = string(in.String()) case "remoteToken": - if in.IsNull() { - in.Skip() - } else { - out.RemoteToken = string(in.String()) - } + out.RemoteToken = string(in.String()) case "hostname": - if in.IsNull() { - in.Skip() - } else { - out.Hostname = string(in.String()) - } + out.Hostname = string(in.String()) case "port": - if in.IsNull() { - in.Skip() - } else { - out.Port = int(in.Int()) - } + out.Port = int(in.Int()) case "rtcpPort": - if in.IsNull() { - in.Skip() - } else { - out.RtcpPort = int(in.Int()) - } + out.RtcpPort = int(in.Int()) default: in.SkipRecursive() } @@ -1649,7 +1681,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10( in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10(out *jwriter.Writer, in CommandClientMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling13(out *jwriter.Writer, in CommandProxyClientMessage) { out.RawByte('{') first := true _ = first @@ -1681,7 +1713,7 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10( if in.Bitrate != 0 { const prefix string = ",\"bitrate\":" out.RawString(prefix) - out.Uint64(uint64(in.Bitrate)) + out.Int(int(in.Bitrate)) } if in.MediaTypes != 0 { const prefix string = ",\"mediatypes\":" @@ -1691,7 +1723,7 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10( if in.PublisherSettings != nil { const prefix string = ",\"publisherSettings\":" out.RawString(prefix) - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu1(out, *in.PublisherSettings) + (*in.PublisherSettings).MarshalEasyJSON(out) } if in.RemoteUrl != "" { const prefix string = ",\"remoteUrl\":" @@ -1722,29 +1754,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10( } // MarshalJSON supports json.Marshaler interface -func (v CommandClientMessage) MarshalJSON() ([]byte, error) { +func (v CommandProxyClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling13(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v CommandClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10(w, v) +func (v CommandProxyClientMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling13(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *CommandClientMessage) UnmarshalJSON(data []byte) error { +func (v *CommandProxyClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling13(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *CommandClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy10(l, v) +func (v *CommandProxyClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling13(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu1(in *jlexer.Lexer, out *sfu.NewPublisherSettings) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling14(in *jlexer.Lexer, out *ByeProxyServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1757,293 +1789,14 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu1(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() - switch key { - case "bitrate": - if in.IsNull() { - in.Skip() - } else { - out.Bitrate = api.Bandwidth(in.Uint64()) - } - case "mediatypes": - if in.IsNull() { - in.Skip() - } else { - out.MediaTypes = sfu.MediaType(in.Int()) - } - case "audiocodec": - if in.IsNull() { - in.Skip() - } else { - out.AudioCodec = string(in.String()) - } - case "videocodec": - if in.IsNull() { - in.Skip() - } else { - out.VideoCodec = string(in.String()) - } - case "vp9_profile": - if in.IsNull() { - in.Skip() - } else { - out.VP9Profile = string(in.String()) - } - case "h264_profile": - if in.IsNull() { - in.Skip() - } else { - out.H264Profile = string(in.String()) - } - default: - in.SkipRecursive() + if in.IsNull() { + in.Skip() + in.WantComma() + continue } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Sfu1(out *jwriter.Writer, in sfu.NewPublisherSettings) { - out.RawByte('{') - first := true - _ = first - if in.Bitrate != 0 { - const prefix string = ",\"bitrate\":" - first = false - out.RawString(prefix[1:]) - out.Uint64(uint64(in.Bitrate)) - } - if in.MediaTypes != 0 { - const prefix string = ",\"mediatypes\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Int(int(in.MediaTypes)) - } - if in.AudioCodec != "" { - const prefix string = ",\"audiocodec\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.AudioCodec)) - } - if in.VideoCodec != "" { - const prefix string = ",\"videocodec\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.VideoCodec)) - } - if in.VP9Profile != "" { - const prefix string = ",\"vp9_profile\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.VP9Profile)) - } - if in.H264Profile != "" { - const prefix string = ",\"h264_profile\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.H264Profile)) - } - out.RawByte('}') -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy11(in *jlexer.Lexer, out *ClientMessage) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "id": - if in.IsNull() { - in.Skip() - } else { - out.Id = string(in.String()) - } - case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } - case "hello": - if in.IsNull() { - in.Skip() - out.Hello = nil - } else { - if out.Hello == nil { - out.Hello = new(HelloClientMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Hello).UnmarshalEasyJSON(in) - } - } - case "bye": - if in.IsNull() { - in.Skip() - out.Bye = nil - } else { - if out.Bye == nil { - out.Bye = new(ByeClientMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Bye).UnmarshalEasyJSON(in) - } - } - case "command": - if in.IsNull() { - in.Skip() - out.Command = nil - } else { - if out.Command == nil { - out.Command = new(CommandClientMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Command).UnmarshalEasyJSON(in) - } - } - case "payload": - if in.IsNull() { - in.Skip() - out.Payload = nil - } else { - if out.Payload == nil { - out.Payload = new(PayloadClientMessage) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Payload).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy11(out *jwriter.Writer, in ClientMessage) { - out.RawByte('{') - first := true - _ = first - if in.Id != "" { - const prefix string = ",\"id\":" - first = false - out.RawString(prefix[1:]) - out.String(string(in.Id)) - } - { - const prefix string = ",\"type\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.Type)) - } - if in.Hello != nil { - const prefix string = ",\"hello\":" - out.RawString(prefix) - (*in.Hello).MarshalEasyJSON(out) - } - if in.Bye != nil { - const prefix string = ",\"bye\":" - out.RawString(prefix) - (*in.Bye).MarshalEasyJSON(out) - } - if in.Command != nil { - const prefix string = ",\"command\":" - out.RawString(prefix) - (*in.Command).MarshalEasyJSON(out) - } - if in.Payload != nil { - const prefix string = ",\"payload\":" - out.RawString(prefix) - (*in.Payload).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v ClientMessage) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy11(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v ClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy11(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *ClientMessage) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy11(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *ClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy11(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy12(in *jlexer.Lexer, out *ByeServerMessage) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() switch key { case "reason": - if in.IsNull() { - in.Skip() - } else { - out.Reason = string(in.String()) - } + out.Reason = string(in.String()) default: in.SkipRecursive() } @@ -2054,7 +1807,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy12( in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy12(out *jwriter.Writer, in ByeServerMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling14(out *jwriter.Writer, in ByeProxyServerMessage) { out.RawByte('{') first := true _ = first @@ -2067,29 +1820,29 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy12( } // MarshalJSON supports json.Marshaler interface -func (v ByeServerMessage) MarshalJSON() ([]byte, error) { +func (v ByeProxyServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy12(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling14(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v ByeServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy12(w, v) +func (v ByeProxyServerMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling14(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *ByeServerMessage) UnmarshalJSON(data []byte) error { +func (v *ByeProxyServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy12(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling14(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *ByeServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy12(l, v) +func (v *ByeProxyServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling14(l, v) } -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13(in *jlexer.Lexer, out *ByeClientMessage) { +func easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling15(in *jlexer.Lexer, out *ByeProxyClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2102,6 +1855,11 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13( for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { default: in.SkipRecursive() @@ -2113,7 +1871,7 @@ func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13( in.Consumed() } } -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13(out *jwriter.Writer, in ByeClientMessage) { +func easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling15(out *jwriter.Writer, in ByeProxyClientMessage) { out.RawByte('{') first := true _ = first @@ -2121,25 +1879,25 @@ func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13( } // MarshalJSON supports json.Marshaler interface -func (v ByeClientMessage) MarshalJSON() ([]byte, error) { +func (v ByeProxyClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13(&w, v) + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling15(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface -func (v ByeClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13(w, v) +func (v ByeProxyClientMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson1c8542dbEncodeGithubComStrukturagNextcloudSpreedSignaling15(w, v) } // UnmarshalJSON supports json.Unmarshaler interface -func (v *ByeClientMessage) UnmarshalJSON(data []byte) error { +func (v *ByeProxyClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13(&r, v) + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling15(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *ByeClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Proxy13(l, v) +func (v *ByeProxyClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson1c8542dbDecodeGithubComStrukturagNextcloudSpreedSignaling15(l, v) } diff --git a/api/signaling.go b/api_signaling.go similarity index 62% rename from api/signaling.go rename to api_signaling.go index 8effa1a..9f978aa 100644 --- a/api/signaling.go +++ b/api_signaling.go @@ -19,26 +19,20 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package api +package signaling import ( "encoding/json" "errors" "fmt" "log" - "net" "net/url" - "slices" + "sort" "strings" "time" "github.com/golang-jwt/jwt/v5" - "github.com/pion/ice/v4" "github.com/pion/sdp/v3" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" ) const ( @@ -53,54 +47,24 @@ const ( ) var ( - // InvalidHelloVersion is returned if the version in the "hello" message is not supported. - InvalidHelloVersion = NewError("invalid_hello_version", "The hello version is not supported.") - - ErrNoSdp = NewError("no_sdp", "Payload does not contain a SDP.") // +checklocksignore: Global readonly variable. + ErrNoSdp = NewError("no_sdp", "Payload does not contain a SDP.") ErrInvalidSdp = NewError("invalid_sdp", "Payload does not contain a valid SDP.") - - ErrNoCandidate = NewError("no_candidate", "Payload does not contain a candidate.") - ErrInvalidCandidate = NewError("invalid_candidate", "Payload does not contain a valid candidate.") - - ErrCandidateFiltered = errors.New("candidate was filtered") ) -type PrivateSessionId string - -type PublicSessionId string - -type RoomSessionId string - -const ( - FederatedRoomSessionIdPrefix = "federated|" -) - -func (s RoomSessionId) IsFederated() bool { - return strings.HasPrefix(string(s), FederatedRoomSessionIdPrefix) +func makePtr[T any](v T) *T { + return &v } -func (s RoomSessionId) WithoutFederation() RoomSessionId { - return RoomSessionId(strings.TrimPrefix(string(s), FederatedRoomSessionIdPrefix)) -} - -type Permission string - -var ( - PERMISSION_MAY_PUBLISH_MEDIA Permission = "publish-media" - PERMISSION_MAY_PUBLISH_AUDIO Permission = "publish-audio" - PERMISSION_MAY_PUBLISH_VIDEO Permission = "publish-video" - PERMISSION_MAY_PUBLISH_SCREEN Permission = "publish-screen" - PERMISSION_MAY_CONTROL Permission = "control" - PERMISSION_TRANSIENT_DATA Permission = "transient-data" - PERMISSION_HIDE_DISPLAYNAMES Permission = "hide-displaynames" - - // DefaultPermissionOverrides contains permission overrides for users where - // no permissions have been set by the server. If a permission is not set in - // this map, it's assumed the user has that permission. - DefaultPermissionOverrides = map[Permission]bool{ // +checklocksignore: Global readonly variable. - PERMISSION_HIDE_DISPLAYNAMES: false, +func getStringMapEntry[T any](m map[string]interface{}, key string) (s T, ok bool) { + var defaultValue T + v, found := m[key] + if !found { + return defaultValue, false } -) + + s, ok = v.(T) + return +} // ClientMessage is a message that is sent from a client to the server. type ClientMessage struct { @@ -132,10 +96,10 @@ type ClientMessage struct { func (m *ClientMessage) CheckValid() error { switch m.Type { case "": - return errors.New("type missing") + return fmt.Errorf("type missing") case "hello": if m.Hello == nil { - return errors.New("hello missing") + return fmt.Errorf("hello missing") } else if err := m.Hello.CheckValid(); err != nil { return err } @@ -143,31 +107,31 @@ func (m *ClientMessage) CheckValid() error { // No additional check required. case "room": if m.Room == nil { - return errors.New("room missing") + return fmt.Errorf("room missing") } else if err := m.Room.CheckValid(); err != nil { return err } case "message": if m.Message == nil { - return errors.New("message missing") + return fmt.Errorf("message missing") } else if err := m.Message.CheckValid(); err != nil { return err } case "control": if m.Control == nil { - return errors.New("control missing") + return fmt.Errorf("control missing") } else if err := m.Control.CheckValid(); err != nil { return err } case "internal": if m.Internal == nil { - return errors.New("internal missing") + return fmt.Errorf("internal missing") } else if err := m.Internal.CheckValid(); err != nil { return err } case "transient": if m.TransientData == nil { - return errors.New("transient missing") + return fmt.Errorf("transient missing") } else if err := m.TransientData.CheckValid(); err != nil { return err } @@ -231,11 +195,7 @@ type ServerMessage struct { Dialout *DialoutInternalClientMessage `json:"dialout,omitempty"` } -type RoomAware interface { - IsInRoom(id string) bool -} - -func (r *ServerMessage) CloseAfterSend(session RoomAware) bool { +func (r *ServerMessage) CloseAfterSend(session Session) bool { if r.Type == "bye" { return true } @@ -244,8 +204,10 @@ func (r *ServerMessage) CloseAfterSend(session RoomAware) bool { if evt := r.Event; evt != nil && evt.Target == "roomlist" && evt.Type == "disinvite" { // Only close session / connection if the disinvite was for the room // the session is currently in. - if session != nil && evt.Disinvite != nil && session.IsInRoom(evt.Disinvite.RoomId) { - return true + if session != nil && evt.Disinvite != nil { + if room := session.GetRoom(); room != nil && evt.Disinvite.RoomId == room.Id() { + return true + } } } } @@ -254,13 +216,12 @@ func (r *ServerMessage) CloseAfterSend(session RoomAware) bool { } func (r *ServerMessage) IsChatRefresh() bool { - if r.Type != "event" || r.Event == nil || - r.Event.Type != "message" || r.Event.Message == nil || len(r.Event.Message.Data) == 0 { + if r.Type != "message" || r.Message == nil || len(r.Message.Data) == 0 { return false } - data, err := r.Event.Message.GetData() - if data == nil || err != nil { + var data MessageServerMessageData + if err := json.Unmarshal(r.Message.Data, &data); err != nil { return false } @@ -299,7 +260,7 @@ func NewError(code string, message string) *Error { return NewErrorDetail(code, message, nil) } -func NewErrorDetail(code string, message string, details any) *Error { +func NewErrorDetail(code string, message string, details interface{}) *Error { var rawDetails json.RawMessage if details != nil { var err error @@ -321,9 +282,9 @@ func (e *Error) Error() string { } type WelcomeServerMessage struct { - Version string `json:"version"` - Features []string `json:"features,omitempty"` - Country geoip.Country `json:"country,omitempty"` + Version string `json:"version"` + Features []string `json:"features,omitempty"` + Country string `json:"country,omitempty"` } func NewWelcomeServerMessage(version string, feature ...string) *WelcomeServerMessage { @@ -332,19 +293,27 @@ func NewWelcomeServerMessage(version string, feature ...string) *WelcomeServerMe Features: feature, } if len(feature) > 0 { - slices.Sort(message.Features) + sort.Strings(message.Features) } return message } func (m *WelcomeServerMessage) AddFeature(feature ...string) { - newFeatures := slices.Clone(m.Features) + newFeatures := make([]string, len(m.Features)) + copy(newFeatures, m.Features) for _, feat := range feature { - if !slices.Contains(newFeatures, feat) { + found := false + for _, f := range newFeatures { + if f == feat { + found = true + break + } + } + if !found { newFeatures = append(newFeatures, feat) } } - slices.Sort(newFeatures) + sort.Strings(newFeatures) m.Features = newFeatures } @@ -352,9 +321,9 @@ func (m *WelcomeServerMessage) RemoveFeature(feature ...string) { newFeatures := make([]string, len(m.Features)) copy(newFeatures, m.Features) for _, feat := range feature { - idx, found := slices.BinarySearch(newFeatures, feat) - if found { - newFeatures = slices.Delete(newFeatures, idx, idx+1) + idx := sort.SearchStrings(newFeatures, feat) + if idx < len(newFeatures) && newFeatures[idx] == feat { + newFeatures = append(newFeatures[:idx], newFeatures[idx+1:]...) } } m.Features = newFeatures @@ -371,41 +340,44 @@ func (m *WelcomeServerMessage) HasFeature(feature string) bool { return false } -type ClientType string - const ( - HelloClientTypeClient = ClientType("client") - HelloClientTypeInternal = ClientType("internal") - HelloClientTypeFederation = ClientType("federation") + HelloClientTypeClient = "client" + HelloClientTypeInternal = "internal" + HelloClientTypeFederation = "federation" - HelloClientTypeVirtual = ClientType("virtual") + HelloClientTypeVirtual = "virtual" ) +func hasStandardPort(u *url.URL) bool { + switch u.Scheme { + case "http": + return u.Port() == "80" + case "https": + return u.Port() == "443" + default: + return false + } +} + type ClientTypeInternalAuthParams struct { Random string `json:"random"` Token string `json:"token"` - Backend string `json:"backend"` - ParsedBackend *url.URL `json:"-"` + Backend string `json:"backend"` + parsedBackend *url.URL } func (p *ClientTypeInternalAuthParams) CheckValid() error { if p.Backend == "" { - return errors.New("backend missing") - } - - if p.Backend[len(p.Backend)-1] != '/' { - p.Backend += "/" - } - if u, err := url.Parse(p.Backend); err != nil { + return fmt.Errorf("backend missing") + } else if u, err := url.Parse(p.Backend); err != nil { return err } else { - var changed bool - if u, changed = internal.CanonicalizeUrl(u); changed { - p.Backend = u.String() + if strings.Contains(u.Host, ":") && hasStandardPort(u) { + u.Host = u.Hostname() } - p.ParsedBackend = u + p.parsedBackend = u } return nil } @@ -416,7 +388,7 @@ type HelloV2AuthParams struct { func (p *HelloV2AuthParams) CheckValid() error { if p.Token == "" { - return errors.New("token missing") + return fmt.Errorf("token missing") } return nil } @@ -443,7 +415,7 @@ type FederationAuthParams struct { func (p *FederationAuthParams) CheckValid() error { if p.Token == "" { - return errors.New("token missing") + return fmt.Errorf("token missing") } return nil } @@ -461,16 +433,16 @@ func (c *FederationTokenClaims) GetUserData() json.RawMessage { type HelloClientMessageAuth struct { // The client type that is connecting. Leave empty to use the default // "HelloClientTypeClient" - Type ClientType `json:"type,omitempty"` + Type string `json:"type,omitempty"` Params json.RawMessage `json:"params"` - Url string `json:"url"` - ParsedUrl *url.URL `json:"-"` + Url string `json:"url"` + parsedUrl *url.URL - InternalParams ClientTypeInternalAuthParams `json:"-"` - HelloV2Params HelloV2AuthParams `json:"-"` - FederationParams FederationAuthParams `json:"-"` + internalParams ClientTypeInternalAuthParams + helloV2Params HelloV2AuthParams + federationParams FederationAuthParams } // Type "hello" @@ -478,7 +450,7 @@ type HelloClientMessageAuth struct { type HelloClientMessage struct { Version string `json:"version"` - ResumeId PrivateSessionId `json:"resumeid"` + ResumeId string `json:"resumeid"` Features []string `json:"features,omitempty"` @@ -492,7 +464,7 @@ func (m *HelloClientMessage) CheckValid() error { } if m.ResumeId == "" { if m.Auth == nil || len(m.Auth.Params) == 0 { - return errors.New("params missing") + return fmt.Errorf("params missing") } if m.Auth.Type == "" { m.Auth.Type = HelloClientTypeClient @@ -502,25 +474,15 @@ func (m *HelloClientMessage) CheckValid() error { fallthrough case HelloClientTypeFederation: if m.Auth.Url == "" { - return errors.New("url missing") - } - - if m.Auth.Url[len(m.Auth.Url)-1] != '/' { - m.Auth.Url += "/" - } - if pos := strings.Index(m.Auth.Url, "ocs/v2.php/apps/spreed/"); pos != -1 { - m.Auth.Url = m.Auth.Url[:pos] - } - - if u, err := url.ParseRequestURI(m.Auth.Url); err != nil { + return fmt.Errorf("url missing") + } else if u, err := url.ParseRequestURI(m.Auth.Url); err != nil { return err } else { - var changed bool - if u, changed = internal.CanonicalizeUrl(u); changed { - m.Auth.Url = u.String() + if strings.Contains(u.Host, ":") && hasStandardPort(u) { + u.Host = u.Hostname() } - m.Auth.ParsedUrl = u + m.Auth.parsedUrl = u } switch m.Version { @@ -529,27 +491,27 @@ func (m *HelloClientMessage) CheckValid() error { case HelloVersionV2: switch m.Auth.Type { case HelloClientTypeClient: - if err := json.Unmarshal(m.Auth.Params, &m.Auth.HelloV2Params); err != nil { + if err := json.Unmarshal(m.Auth.Params, &m.Auth.helloV2Params); err != nil { return err - } else if err := m.Auth.HelloV2Params.CheckValid(); err != nil { + } else if err := m.Auth.helloV2Params.CheckValid(); err != nil { return err } case HelloClientTypeFederation: - if err := json.Unmarshal(m.Auth.Params, &m.Auth.FederationParams); err != nil { + if err := json.Unmarshal(m.Auth.Params, &m.Auth.federationParams); err != nil { return err - } else if err := m.Auth.FederationParams.CheckValid(); err != nil { + } else if err := m.Auth.federationParams.CheckValid(); err != nil { return err } } } case HelloClientTypeInternal: - if err := json.Unmarshal(m.Auth.Params, &m.Auth.InternalParams); err != nil { + if err := json.Unmarshal(m.Auth.Params, &m.Auth.internalParams); err != nil { return err - } else if err := m.Auth.InternalParams.CheckValid(); err != nil { + } else if err := m.Auth.internalParams.CheckValid(); err != nil { return err } default: - return errors.New("unsupported auth type") + return fmt.Errorf("unsupported auth type") } } return nil @@ -571,15 +533,11 @@ const ( ServerFeatureRecipientCall = "recipient-call" ServerFeatureJoinFeatures = "join-features" ServerFeatureOfferCodecs = "offer-codecs" - ServerFeatureServerInfo = "serverinfo" - ServerFeatureChatRelay = "chat-relay" - ServerFeatureTransientSessionData = "transient-sessiondata" // Features to send to internal clients only. ServerFeatureInternalVirtualSessions = "virtual-sessions" // Possible client features from the "hello" request. - ClientFeatureChatRelay = "chat-relay" ClientFeatureInternalInCall = "internal-incall" ClientFeatureStartDialout = "start-dialout" ) @@ -597,9 +555,6 @@ var ( ServerFeatureRecipientCall, ServerFeatureJoinFeatures, ServerFeatureOfferCodecs, - ServerFeatureServerInfo, - ServerFeatureChatRelay, - ServerFeatureTransientSessionData, } DefaultFeaturesInternal = []string{ ServerFeatureInternalVirtualSessions, @@ -613,9 +568,6 @@ var ( ServerFeatureRecipientCall, ServerFeatureJoinFeatures, ServerFeatureOfferCodecs, - ServerFeatureServerInfo, - ServerFeatureChatRelay, - ServerFeatureTransientSessionData, } DefaultWelcomeFeatures = []string{ ServerFeatureAudioVideoPermissions, @@ -630,18 +582,15 @@ var ( ServerFeatureRecipientCall, ServerFeatureJoinFeatures, ServerFeatureOfferCodecs, - ServerFeatureServerInfo, - ServerFeatureChatRelay, - ServerFeatureTransientSessionData, } ) type HelloServerMessage struct { Version string `json:"version"` - SessionId PublicSessionId `json:"sessionid"` - ResumeId PrivateSessionId `json:"resumeid"` - UserId string `json:"userid"` + SessionId string `json:"sessionid"` + ResumeId string `json:"resumeid"` + UserId string `json:"userid"` // TODO: Remove once all clients have switched to the "welcome" message. Server *WelcomeServerMessage `json:"server,omitempty"` @@ -664,8 +613,8 @@ type ByeServerMessage struct { // Type "room" type RoomClientMessage struct { - RoomId string `json:"roomid"` - SessionId RoomSessionId `json:"sessionid,omitempty"` + RoomId string `json:"roomid"` + SessionId string `json:"sessionid,omitempty"` Federation *RoomFederationMessage `json:"federation,omitempty"` } @@ -682,11 +631,11 @@ func (m *RoomClientMessage) CheckValid() error { } type RoomFederationMessage struct { - SignalingUrl string `json:"signaling"` - ParsedSignalingUrl *url.URL `json:"-"` + SignalingUrl string `json:"signaling"` + parsedSignalingUrl *url.URL - NextcloudUrl string `json:"url"` - ParsedNextcloudUrl *url.URL `json:"-"` + NextcloudUrl string `json:"url"` + parsedNextcloudUrl *url.URL RoomId string `json:"roomid,omitempty"` Token string `json:"token"` @@ -703,14 +652,14 @@ func (m *RoomFederationMessage) CheckValid() error { if u, err := url.Parse(m.SignalingUrl); err != nil { return fmt.Errorf("invalid signaling url: %w", err) } else { - m.ParsedSignalingUrl = u + m.parsedSignalingUrl = u } if m.NextcloudUrl == "" { return errors.New("nextcloud url missing") } else if u, err := url.Parse(m.NextcloudUrl); err != nil { return fmt.Errorf("invalid nextcloud url: %w", err) } else { - m.ParsedNextcloudUrl = u + m.parsedNextcloudUrl = u } if m.Token == "" { return errors.New("token missing") @@ -722,12 +671,6 @@ func (m *RoomFederationMessage) CheckValid() error { type RoomServerMessage struct { RoomId string `json:"roomid"` Properties json.RawMessage `json:"properties,omitempty"` - Bandwidth *RoomBandwidth `json:"bandwidth,omitempty"` -} - -type RoomBandwidth struct { - MaxStreamBitrate Bandwidth `json:"maxstreambitrate"` - MaxScreenBitrate Bandwidth `json:"maxscreenbitrate"` } type RoomErrorDetails struct { @@ -746,8 +689,8 @@ const ( type MessageClientMessageRecipient struct { Type string `json:"type"` - SessionId PublicSessionId `json:"sessionid,omitempty"` - UserId string `json:"userid,omitempty"` + SessionId string `json:"sessionid,omitempty"` + UserId string `json:"userid,omitempty"` } type MessageClientMessage struct { @@ -757,186 +700,56 @@ type MessageClientMessage struct { } type MessageClientMessageData struct { - json.Marshaler - json.Unmarshaler - - Type string `json:"type"` - Sid string `json:"sid"` - RoomType string `json:"roomType"` - Payload StringMap `json:"payload"` + Type string `json:"type"` + Sid string `json:"sid"` + RoomType string `json:"roomType"` + Payload map[string]interface{} `json:"payload"` // Only supported if Type == "offer" - Bitrate Bandwidth `json:"bitrate,omitempty"` - AudioCodec string `json:"audiocodec,omitempty"` - VideoCodec string `json:"videocodec,omitempty"` - VP9Profile string `json:"vp9profile,omitempty"` - H264Profile string `json:"h264profile,omitempty"` + Bitrate int `json:"bitrate,omitempty"` + AudioCodec string `json:"audiocodec,omitempty"` + VideoCodec string `json:"videocodec,omitempty"` + VP9Profile string `json:"vp9profile,omitempty"` + H264Profile string `json:"h264profile,omitempty"` - OfferSdp *sdp.SessionDescription `json:"-"` // Only set if Type == "offer" - AnswerSdp *sdp.SessionDescription `json:"-"` // Only set if Type == "answer" - Candidate ice.Candidate `json:"-"` // Only set if Type == "candidate" -} - -func (m *MessageClientMessageData) String() string { - data, err := json.Marshal(m) - if err != nil { - return fmt.Sprintf("Could not serialize %#v: %s", m, err) - } - return string(data) -} - -func ParseSDP(s string) (*sdp.SessionDescription, error) { - var sdp sdp.SessionDescription - if err := sdp.UnmarshalString(s); err != nil { - return nil, NewErrorDetail("invalid_sdp", "Error parsing SDP from payload.", StringMap{ - "error": err.Error(), - }) - } - - for _, m := range sdp.MediaDescriptions { - for idx, a := range m.Attributes { - if !a.IsICECandidate() { - continue - } - - if _, err := ice.UnmarshalCandidate(a.Value); err != nil { - return nil, NewErrorDetail("invalid_sdp", "Error parsing candidate from media description.", StringMap{ - "media": m.MediaName.Media, - "idx": idx, - "error": err.Error(), - }) - } - } - } - - return &sdp, nil -} - -var ( - emptyCandidate = &ice.CandidateHost{} -) - -// TODO: Use shared method from "mcu_common.go". -func isValidStreamType(s string) bool { - switch s { - case "audio": - fallthrough - case "video": - fallthrough - case "screen": - return true - default: - return false - } + offerSdp *sdp.SessionDescription // Only set if Type == "offer" + answerSdp *sdp.SessionDescription // Only set if Type == "answer" } func (m *MessageClientMessageData) CheckValid() error { - if m.RoomType != "" && !isValidStreamType(m.RoomType) { + if m.RoomType != "" && !IsValidStreamType(m.RoomType) { return fmt.Errorf("invalid room type: %s", m.RoomType) } - switch m.Type { - case "": - return errors.New("type missing") - case "offer", "answer": - sdpText, ok := GetStringMapEntry[string](m.Payload, "sdp") + if m.Type == "offer" || m.Type == "answer" { + sdpValue, found := m.Payload["sdp"] + if !found { + return ErrNoSdp + } + sdpText, ok := sdpValue.(string) if !ok { return ErrInvalidSdp } - sdp, err := ParseSDP(sdpText) - if err != nil { - return err + var sdp sdp.SessionDescription + if err := sdp.Unmarshal([]byte(sdpText)); err != nil { + return NewErrorDetail("invalid_sdp", "Error parsing SDP from payload.", map[string]interface{}{ + "error": err.Error(), + }) } switch m.Type { case "offer": - m.OfferSdp = sdp + m.offerSdp = &sdp case "answer": - m.AnswerSdp = sdp - } - case "candidate": - candValue, found := m.Payload["candidate"] - if !found { - return ErrNoCandidate - } - candItem, ok := ConvertStringMap(candValue) - if !ok { - return ErrInvalidCandidate - } - candText, ok := GetStringMapEntry[string](candItem, "candidate") - if !ok { - return ErrInvalidCandidate - } - - if candText == "" { - m.Candidate = emptyCandidate - } else { - cand, err := ice.UnmarshalCandidate(candText) - if err != nil { - return NewErrorDetail("invalid_candidate", "Error parsing candidate from payload.", StringMap{ - "error": err.Error(), - }) - } - m.Candidate = cand + m.answerSdp = &sdp } } return nil } -func FilterCandidate(c ice.Candidate, allowed *container.IPList, blocked *container.IPList) bool { - switch c { - case nil: - return true - case emptyCandidate: - return false - } - - ip := net.ParseIP(c.Address()) - if len(ip) == 0 || ip.IsUnspecified() { - return true - } - - // Whitelist has preference. - if allowed != nil && allowed.Contains(ip) { - return false - } - - // Check if address is blocked manually. - if blocked != nil && blocked.Contains(ip) { - return true - } - - return false -} - -func FilterSDPCandidates(s *sdp.SessionDescription, allowed *container.IPList, blocked *container.IPList) bool { - modified := false - for _, m := range s.MediaDescriptions { - m.Attributes = slices.DeleteFunc(m.Attributes, func(a sdp.Attribute) bool { - if !a.IsICECandidate() { - return false - } - - if a.Value == "" { - return false - } - - c, err := ice.UnmarshalCandidate(a.Value) - if err != nil || FilterCandidate(c, allowed, blocked) { - modified = true - return true - } - - return false - }) - } - - return modified -} - func (m *MessageClientMessage) CheckValid() error { if len(m.Data) == 0 { - return errors.New("message empty") + return fmt.Errorf("message empty") } switch m.Recipient.Type { case RecipientTypeRoom: @@ -945,11 +758,11 @@ func (m *MessageClientMessage) CheckValid() error { // No additional checks required. case RecipientTypeSession: if m.Recipient.SessionId == "" { - return errors.New("session id missing") + return fmt.Errorf("session id missing") } case RecipientTypeUser: if m.Recipient.UserId == "" { - return errors.New("user id missing") + return fmt.Errorf("user id missing") } default: return fmt.Errorf("unsupported recipient type %v", m.Recipient.Type) @@ -960,12 +773,18 @@ func (m *MessageClientMessage) CheckValid() error { type MessageServerMessageSender struct { Type string `json:"type"` - SessionId PublicSessionId `json:"sessionid,omitempty"` - UserId string `json:"userid,omitempty"` + SessionId string `json:"sessionid,omitempty"` + UserId string `json:"userid,omitempty"` +} + +type MessageServerMessageDataChat struct { + Refresh bool `json:"refresh"` } type MessageServerMessageData struct { Type string `json:"type"` + + Chat *MessageServerMessageDataChat `json:"chat,omitempty"` } type MessageServerMessage struct { @@ -995,17 +814,17 @@ type ControlServerMessage struct { // Type "internal" type CommonSessionInternalClientMessage struct { - SessionId PublicSessionId `json:"sessionid"` + SessionId string `json:"sessionid"` RoomId string `json:"roomid"` } func (m *CommonSessionInternalClientMessage) CheckValid() error { if m.SessionId == "" { - return errors.New("sessionid missing") + return fmt.Errorf("sessionid missing") } if m.RoomId == "" { - return errors.New("roomid missing") + return fmt.Errorf("roomid missing") } return nil } @@ -1125,31 +944,31 @@ func (m *InternalClientMessage) CheckValid() error { return errors.New("type missing") case "addsession": if m.AddSession == nil { - return errors.New("addsession missing") + return fmt.Errorf("addsession missing") } else if err := m.AddSession.CheckValid(); err != nil { return err } case "updatesession": if m.UpdateSession == nil { - return errors.New("updatesession missing") + return fmt.Errorf("updatesession missing") } else if err := m.UpdateSession.CheckValid(); err != nil { return err } case "removesession": if m.RemoveSession == nil { - return errors.New("removesession missing") + return fmt.Errorf("removesession missing") } else if err := m.RemoveSession.CheckValid(); err != nil { return err } case "incall": if m.InCall == nil { - return errors.New("incall missing") + return fmt.Errorf("incall missing") } else if err := m.InCall.CheckValid(); err != nil { return err } case "dialout": if m.Dialout == nil { - return errors.New("dialout missing") + return fmt.Errorf("dialout missing") } else if err := m.Dialout.CheckValid(); err != nil { return err } @@ -1157,18 +976,11 @@ func (m *InternalClientMessage) CheckValid() error { return nil } -type InternalServerDialoutRequestContents struct { - // E.164 number to dial (e.g. "+1234567890") - Number string `json:"number"` - - Options json.RawMessage `json:"options,omitempty"` -} - type InternalServerDialoutRequest struct { RoomId string `json:"roomid"` Backend string `json:"backend"` - Request *InternalServerDialoutRequestContents `json:"request"` + Request *BackendRoomDialoutRequest `json:"request"` } type InternalServerMessage struct { @@ -1183,9 +995,9 @@ type RoomEventServerMessage struct { RoomId string `json:"roomid"` Properties json.RawMessage `json:"properties,omitempty"` // TODO(jojo): Change "InCall" to "int" when #914 has landed in NC Talk. - InCall json.RawMessage `json:"incall,omitempty"` - Changed []StringMap `json:"changed,omitempty"` - Users []StringMap `json:"users,omitempty"` + InCall json.RawMessage `json:"incall,omitempty"` + Changed []map[string]interface{} `json:"changed,omitempty"` + Users []map[string]interface{} `json:"users,omitempty"` All bool `json:"all,omitempty"` } @@ -1209,22 +1021,21 @@ type RoomDisinviteEventServerMessage struct { Reason string `json:"reason"` } -type ChatComment StringMap - -type RoomEventMessageDataChat struct { - // Refresh will be included if the client does not support the "chat-relay" feature. - Refresh bool `json:"refresh,omitempty"` - - // Comment will be included if the client supports the "chat-relay" feature. - Comment json.RawMessage `json:"comment,omitempty"` - // Comments will be included if the client supports the "chat-relay" feature. - Comments []json.RawMessage `json:"comments,omitempty"` +type RoomEventMessage struct { + RoomId string `json:"roomid"` + Data json.RawMessage `json:"data,omitempty"` } -func (m *RoomEventMessageDataChat) HasComment() bool { - return len(m.Comment) > 0 || slices.ContainsFunc(m.Comments, func(comment json.RawMessage) bool { - return len(comment) > 0 - }) +type RoomFlagsServerMessage struct { + RoomId string `json:"roomid"` + SessionId string `json:"sessionid"` + Flags uint32 `json:"flags"` +} + +type ChatComment map[string]interface{} + +type RoomEventMessageDataChat struct { + Comment *ChatComment `json:"comment,omitempty"` } type RoomEventMessageData struct { @@ -1233,41 +1044,16 @@ type RoomEventMessageData struct { Chat *RoomEventMessageDataChat `json:"chat,omitempty"` } -type RoomEventMessage struct { - RoomId string `json:"roomid"` - Data json.RawMessage `json:"data,omitempty"` -} - -func (m *RoomEventMessage) GetData() (*RoomEventMessageData, error) { - if len(m.Data) == 0 { - return nil, nil - } - - // TODO: Cache parsed result. - var data RoomEventMessageData - if err := json.Unmarshal(m.Data, &data); err != nil { - return nil, err - } - - return &data, nil -} - -type RoomFlagsServerMessage struct { - RoomId string `json:"roomid"` - SessionId PublicSessionId `json:"sessionid"` - Flags uint32 `json:"flags"` -} - type EventServerMessage struct { Target string `json:"target"` Type string `json:"type"` // Used for target "room" - Join []EventServerMessageSessionEntry `json:"join,omitempty"` - Leave []PublicSessionId `json:"leave,omitempty"` - Change []EventServerMessageSessionEntry `json:"change,omitempty"` - SwitchTo *EventServerMessageSwitchTo `json:"switchto,omitempty"` - Resumed *bool `json:"resumed,omitempty"` + Join []*EventServerMessageSessionEntry `json:"join,omitempty"` + Leave []string `json:"leave,omitempty"` + Change []*EventServerMessageSessionEntry `json:"change,omitempty"` + SwitchTo *EventServerMessageSwitchTo `json:"switchto,omitempty"` + Resumed *bool `json:"resumed,omitempty"` // Used for target "roomlist" / "participants" Invite *RoomEventServerMessage `json:"invite,omitempty"` @@ -1288,16 +1074,16 @@ func (m *EventServerMessage) String() string { } type EventServerMessageSessionEntry struct { - SessionId PublicSessionId `json:"sessionid"` + SessionId string `json:"sessionid"` UserId string `json:"userid"` Features []string `json:"features,omitempty"` User json.RawMessage `json:"user,omitempty"` - RoomSessionId RoomSessionId `json:"roomsessionid,omitempty"` + RoomSessionId string `json:"roomsessionid,omitempty"` Federated bool `json:"federated,omitempty"` } -func (e EventServerMessageSessionEntry) Clone() EventServerMessageSessionEntry { - return EventServerMessageSessionEntry{ +func (e *EventServerMessageSessionEntry) Clone() *EventServerMessageSessionEntry { + return &EventServerMessageSessionEntry{ SessionId: e.SessionId, UserId: e.UserId, Features: e.Features, @@ -1315,12 +1101,12 @@ type EventServerMessageSwitchTo struct { // MCU-related types type AnswerOfferMessage struct { - To PublicSessionId `json:"to"` - From PublicSessionId `json:"from"` - Type string `json:"type"` - RoomType string `json:"roomType"` - Payload StringMap `json:"payload"` - Sid string `json:"sid,omitempty"` + To string `json:"to"` + From string `json:"from"` + Type string `json:"type"` + RoomType string `json:"roomType"` + Payload map[string]interface{} `json:"payload"` + Sid string `json:"sid,omitempty"` } // Type "transient" @@ -1335,16 +1121,14 @@ type TransientDataClientMessage struct { func (m *TransientDataClientMessage) CheckValid() error { switch m.Type { - case "": - return errors.New("type missing") case "set": if m.Key == "" { - return errors.New("key missing") + return fmt.Errorf("key missing") } // A "nil" value is allowed and will remove the key. case "remove": if m.Key == "" { - return errors.New("key missing") + return fmt.Errorf("key missing") } } return nil @@ -1353,8 +1137,8 @@ func (m *TransientDataClientMessage) CheckValid() error { type TransientDataServerMessage struct { Type string `json:"type"` - Key string `json:"key,omitempty"` - OldValue any `json:"oldvalue,omitempty"` - Value any `json:"value,omitempty"` - Data StringMap `json:"data,omitempty"` + Key string `json:"key,omitempty"` + OldValue interface{} `json:"oldvalue,omitempty"` + Value interface{} `json:"value,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` } diff --git a/api/signaling_easyjson.go b/api_signaling_easyjson.go similarity index 64% rename from api/signaling_easyjson.go rename to api_signaling_easyjson.go index 877c6e8..86ee338 100644 --- a/api/signaling_easyjson.go +++ b/api_signaling_easyjson.go @@ -1,6 +1,6 @@ // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. -package api +package signaling import ( json "encoding/json" @@ -8,7 +8,6 @@ import ( easyjson "github.com/mailru/easyjson" jlexer "github.com/mailru/easyjson/jlexer" jwriter "github.com/mailru/easyjson/jwriter" - geoip "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" time "time" ) @@ -20,7 +19,7 @@ var ( _ easyjson.Marshaler ) -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api(in *jlexer.Lexer, out *WelcomeServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling(in *jlexer.Lexer, out *WelcomeServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -33,13 +32,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api(in *j for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } + out.Version = string(in.String()) case "features": if in.IsNull() { in.Skip() @@ -57,22 +57,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api(in *j } for !in.IsDelim(']') { var v1 string - if in.IsNull() { - in.Skip() - } else { - v1 = string(in.String()) - } + v1 = string(in.String()) out.Features = append(out.Features, v1) in.WantComma() } in.Delim(']') } case "country": - if in.IsNull() { - in.Skip() - } else { - out.Country = geoip.Country(in.String()) - } + out.Country = string(in.String()) default: in.SkipRecursive() } @@ -83,7 +75,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api(in *j in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api(out *jwriter.Writer, in WelcomeServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling(out *jwriter.Writer, in WelcomeServerMessage) { out.RawByte('{') first := true _ = first @@ -117,27 +109,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api(out * // MarshalJSON supports json.Marshaler interface func (v WelcomeServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v WelcomeServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *WelcomeServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *WelcomeServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(in *jlexer.Lexer, out *UpdateSessionInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling1(in *jlexer.Lexer, out *UpdateSessionInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -150,6 +142,11 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "flags": if in.IsNull() { @@ -159,11 +156,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(in * if out.Flags == nil { out.Flags = new(uint32) } - if in.IsNull() { - in.Skip() - } else { - *out.Flags = uint32(in.Uint32()) - } + *out.Flags = uint32(in.Uint32()) } case "incall": if in.IsNull() { @@ -173,24 +166,12 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(in * if out.InCall == nil { out.InCall = new(int) } - if in.IsNull() { - in.Skip() - } else { - *out.InCall = int(in.Int()) - } + *out.InCall = int(in.Int()) } case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) default: in.SkipRecursive() } @@ -201,7 +182,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(out *jwriter.Writer, in UpdateSessionInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling1(out *jwriter.Writer, in UpdateSessionInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -242,27 +223,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(out // MarshalJSON supports json.Marshaler interface func (v UpdateSessionInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling1(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v UpdateSessionInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling1(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *UpdateSessionInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling1(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *UpdateSessionInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api1(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling1(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(in *jlexer.Lexer, out *TransientDataServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling2(in *jlexer.Lexer, out *TransientDataServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -275,19 +256,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "key": - if in.IsNull() { - in.Skip() - } else { - out.Key = string(in.String()) - } + out.Key = string(in.String()) case "oldvalue": if m, ok := out.OldValue.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) @@ -310,7 +288,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(in * } else { in.Delim('{') if !in.IsDelim('}') { - out.Data = make(StringMap) + out.Data = make(map[string]interface{}) } else { out.Data = nil } @@ -340,7 +318,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(out *jwriter.Writer, in TransientDataServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling2(out *jwriter.Writer, in TransientDataServerMessage) { out.RawByte('{') first := true _ = first @@ -407,27 +385,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(out // MarshalJSON supports json.Marshaler interface func (v TransientDataServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling2(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v TransientDataServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling2(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *TransientDataServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling2(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *TransientDataServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api2(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling2(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(in *jlexer.Lexer, out *TransientDataClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling3(in *jlexer.Lexer, out *TransientDataClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -440,33 +418,22 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "key": - if in.IsNull() { - in.Skip() - } else { - out.Key = string(in.String()) - } + out.Key = string(in.String()) case "value": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Value).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Value).UnmarshalJSON(data)) } case "ttl": - if in.IsNull() { - in.Skip() - } else { - out.TTL = time.Duration(in.Int64()) - } + out.TTL = time.Duration(in.Int64()) default: in.SkipRecursive() } @@ -477,7 +444,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(out *jwriter.Writer, in TransientDataClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling3(out *jwriter.Writer, in TransientDataClientMessage) { out.RawByte('{') first := true _ = first @@ -507,27 +474,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(out // MarshalJSON supports json.Marshaler interface func (v TransientDataClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling3(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v TransientDataClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling3(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *TransientDataClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling3(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *TransientDataClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api3(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling3(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in *jlexer.Lexer, out *ServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling4(in *jlexer.Lexer, out *ServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -540,19 +507,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "id": - if in.IsNull() { - in.Skip() - } else { - out.Id = string(in.String()) - } + out.Id = string(in.String()) case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "error": if in.IsNull() { in.Skip() @@ -561,11 +525,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Error == nil { out.Error = new(Error) } - if in.IsNull() { - in.Skip() - } else { - (*out.Error).UnmarshalEasyJSON(in) - } + (*out.Error).UnmarshalEasyJSON(in) } case "welcome": if in.IsNull() { @@ -575,11 +535,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Welcome == nil { out.Welcome = new(WelcomeServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Welcome).UnmarshalEasyJSON(in) - } + (*out.Welcome).UnmarshalEasyJSON(in) } case "hello": if in.IsNull() { @@ -589,11 +545,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Hello == nil { out.Hello = new(HelloServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Hello).UnmarshalEasyJSON(in) - } + (*out.Hello).UnmarshalEasyJSON(in) } case "bye": if in.IsNull() { @@ -603,11 +555,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Bye == nil { out.Bye = new(ByeServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Bye).UnmarshalEasyJSON(in) - } + (*out.Bye).UnmarshalEasyJSON(in) } case "room": if in.IsNull() { @@ -617,11 +565,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Room == nil { out.Room = new(RoomServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Room).UnmarshalEasyJSON(in) - } + (*out.Room).UnmarshalEasyJSON(in) } case "message": if in.IsNull() { @@ -631,11 +575,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Message == nil { out.Message = new(MessageServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Message).UnmarshalEasyJSON(in) - } + (*out.Message).UnmarshalEasyJSON(in) } case "control": if in.IsNull() { @@ -645,11 +585,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Control == nil { out.Control = new(ControlServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Control).UnmarshalEasyJSON(in) - } + (*out.Control).UnmarshalEasyJSON(in) } case "event": if in.IsNull() { @@ -659,11 +595,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Event == nil { out.Event = new(EventServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Event).UnmarshalEasyJSON(in) - } + (*out.Event).UnmarshalEasyJSON(in) } case "transient": if in.IsNull() { @@ -673,11 +605,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.TransientData == nil { out.TransientData = new(TransientDataServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.TransientData).UnmarshalEasyJSON(in) - } + (*out.TransientData).UnmarshalEasyJSON(in) } case "internal": if in.IsNull() { @@ -687,11 +615,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Internal == nil { out.Internal = new(InternalServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Internal).UnmarshalEasyJSON(in) - } + (*out.Internal).UnmarshalEasyJSON(in) } case "dialout": if in.IsNull() { @@ -701,11 +625,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * if out.Dialout == nil { out.Dialout = new(DialoutInternalClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Dialout).UnmarshalEasyJSON(in) - } + (*out.Dialout).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -717,7 +637,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(out *jwriter.Writer, in ServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling4(out *jwriter.Writer, in ServerMessage) { out.RawByte('{') first := true _ = first @@ -798,27 +718,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(out // MarshalJSON supports json.Marshaler interface func (v ServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling4(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling4(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling4(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api4(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling4(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(in *jlexer.Lexer, out *RoomServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling5(in *jlexer.Lexer, out *RoomServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -831,34 +751,17 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "properties": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Properties).UnmarshalJSON(data)) - } - } - case "bandwidth": - if in.IsNull() { - in.Skip() - out.Bandwidth = nil - } else { - if out.Bandwidth == nil { - out.Bandwidth = new(RoomBandwidth) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Bandwidth).UnmarshalEasyJSON(in) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Properties).UnmarshalJSON(data)) } default: in.SkipRecursive() @@ -870,7 +773,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(out *jwriter.Writer, in RoomServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling5(out *jwriter.Writer, in RoomServerMessage) { out.RawByte('{') first := true _ = first @@ -884,38 +787,33 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(out out.RawString(prefix) out.Raw((in.Properties).MarshalJSON()) } - if in.Bandwidth != nil { - const prefix string = ",\"bandwidth\":" - out.RawString(prefix) - (*in.Bandwidth).MarshalEasyJSON(out) - } out.RawByte('}') } // MarshalJSON supports json.Marshaler interface func (v RoomServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling5(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling5(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling5(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api5(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling5(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(in *jlexer.Lexer, out *RoomFlagsServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling6(in *jlexer.Lexer, out *RoomFlagsServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -928,25 +826,18 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "flags": - if in.IsNull() { - in.Skip() - } else { - out.Flags = uint32(in.Uint32()) - } + out.Flags = uint32(in.Uint32()) default: in.SkipRecursive() } @@ -957,7 +848,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(out *jwriter.Writer, in RoomFlagsServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling6(out *jwriter.Writer, in RoomFlagsServerMessage) { out.RawByte('{') first := true _ = first @@ -982,27 +873,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(out // MarshalJSON supports json.Marshaler interface func (v RoomFlagsServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling6(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomFlagsServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling6(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomFlagsServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling6(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomFlagsServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api6(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling6(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(in *jlexer.Lexer, out *RoomFederationMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(in *jlexer.Lexer, out *RoomFederationMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1015,31 +906,20 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "signaling": - if in.IsNull() { - in.Skip() - } else { - out.SignalingUrl = string(in.String()) - } + out.SignalingUrl = string(in.String()) case "url": - if in.IsNull() { - in.Skip() - } else { - out.NextcloudUrl = string(in.String()) - } + out.NextcloudUrl = string(in.String()) case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "token": - if in.IsNull() { - in.Skip() - } else { - out.Token = string(in.String()) - } + out.Token = string(in.String()) default: in.SkipRecursive() } @@ -1050,7 +930,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(out *jwriter.Writer, in RoomFederationMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(out *jwriter.Writer, in RoomFederationMessage) { out.RawByte('{') first := true _ = first @@ -1080,27 +960,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(out // MarshalJSON supports json.Marshaler interface func (v RoomFederationMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomFederationMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling7(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomFederationMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomFederationMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api7(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling7(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(in *jlexer.Lexer, out *RoomEventServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(in *jlexer.Lexer, out *RoomEventServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1113,28 +993,21 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "properties": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Properties).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Properties).UnmarshalJSON(data)) } case "incall": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.InCall).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.InCall).UnmarshalJSON(data)) } case "changed": if in.IsNull() { @@ -1144,21 +1017,21 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(in * in.Delim('[') if out.Changed == nil { if !in.IsDelim(']') { - out.Changed = make([]StringMap, 0, 8) + out.Changed = make([]map[string]interface{}, 0, 8) } else { - out.Changed = []StringMap{} + out.Changed = []map[string]interface{}{} } } else { out.Changed = (out.Changed)[:0] } for !in.IsDelim(']') { - var v6 StringMap + var v6 map[string]interface{} if in.IsNull() { in.Skip() } else { in.Delim('{') if !in.IsDelim('}') { - v6 = make(StringMap) + v6 = make(map[string]interface{}) } else { v6 = nil } @@ -1191,21 +1064,21 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(in * in.Delim('[') if out.Users == nil { if !in.IsDelim(']') { - out.Users = make([]StringMap, 0, 8) + out.Users = make([]map[string]interface{}, 0, 8) } else { - out.Users = []StringMap{} + out.Users = []map[string]interface{}{} } } else { out.Users = (out.Users)[:0] } for !in.IsDelim(']') { - var v8 StringMap + var v8 map[string]interface{} if in.IsNull() { in.Skip() } else { in.Delim('{') if !in.IsDelim('}') { - v8 = make(StringMap) + v8 = make(map[string]interface{}) } else { v8 = nil } @@ -1231,11 +1104,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(in * in.Delim(']') } case "all": - if in.IsNull() { - in.Skip() - } else { - out.All = bool(in.Bool()) - } + out.All = bool(in.Bool()) default: in.SkipRecursive() } @@ -1246,7 +1115,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(out *jwriter.Writer, in RoomEventServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(out *jwriter.Writer, in RoomEventServerMessage) { out.RawByte('{') first := true _ = first @@ -1348,27 +1217,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(out // MarshalJSON supports json.Marshaler interface func (v RoomEventServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomEventServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling8(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomEventServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomEventServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api8(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling8(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(in *jlexer.Lexer, out *RoomEventMessageDataChat) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(in *jlexer.Lexer, out *RoomEventMessageDataChat) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1381,49 +1250,45 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(in * for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { - case "refresh": - if in.IsNull() { - in.Skip() - } else { - out.Refresh = bool(in.Bool()) - } case "comment": if in.IsNull() { in.Skip() + out.Comment = nil } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Comment).UnmarshalJSON(data)) + if out.Comment == nil { + out.Comment = new(ChatComment) } - } - case "comments": - if in.IsNull() { - in.Skip() - out.Comments = nil - } else { - in.Delim('[') - if out.Comments == nil { - if !in.IsDelim(']') { - out.Comments = make([]json.RawMessage, 0, 2) - } else { - out.Comments = []json.RawMessage{} - } + if in.IsNull() { + in.Skip() } else { - out.Comments = (out.Comments)[:0] - } - for !in.IsDelim(']') { - var v16 json.RawMessage - if in.IsNull() { - in.Skip() + in.Delim('{') + if !in.IsDelim('}') { + *out.Comment = make(ChatComment) } else { - if data := in.Raw(); in.Ok() { - in.AddError((v16).UnmarshalJSON(data)) - } + *out.Comment = nil } - out.Comments = append(out.Comments, v16) - in.WantComma() + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v16 interface{} + if m, ok := v16.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := v16.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + v16 = in.Interface() + } + (*out.Comment)[key] = v16 + in.WantComma() + } + in.Delim('}') } - in.Delim(']') } default: in.SkipRecursive() @@ -1435,43 +1300,36 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(in * in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(out *jwriter.Writer, in RoomEventMessageDataChat) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(out *jwriter.Writer, in RoomEventMessageDataChat) { out.RawByte('{') first := true _ = first - if in.Refresh { - const prefix string = ",\"refresh\":" + if in.Comment != nil { + const prefix string = ",\"comment\":" first = false out.RawString(prefix[1:]) - out.Bool(bool(in.Refresh)) - } - if len(in.Comment) != 0 { - const prefix string = ",\"comment\":" - if first { - first = false - out.RawString(prefix[1:]) + if *in.Comment == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) } else { - out.RawString(prefix) - } - out.Raw((in.Comment).MarshalJSON()) - } - if len(in.Comments) != 0 { - const prefix string = ",\"comments\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - { - out.RawByte('[') - for v17, v18 := range in.Comments { - if v17 > 0 { + out.RawByte('{') + v17First := true + for v17Name, v17Value := range *in.Comment { + if v17First { + v17First = false + } else { out.RawByte(',') } - out.Raw((v18).MarshalJSON()) + out.String(string(v17Name)) + out.RawByte(':') + if m, ok := v17Value.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := v17Value.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(v17Value)) + } } - out.RawByte(']') + out.RawByte('}') } } out.RawByte('}') @@ -1480,27 +1338,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(out // MarshalJSON supports json.Marshaler interface func (v RoomEventMessageDataChat) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomEventMessageDataChat) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling9(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomEventMessageDataChat) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomEventMessageDataChat) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api9(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling9(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(in *jlexer.Lexer, out *RoomEventMessageData) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(in *jlexer.Lexer, out *RoomEventMessageData) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1513,13 +1371,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "chat": if in.IsNull() { in.Skip() @@ -1528,11 +1387,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(in if out.Chat == nil { out.Chat = new(RoomEventMessageDataChat) } - if in.IsNull() { - in.Skip() - } else { - (*out.Chat).UnmarshalEasyJSON(in) - } + (*out.Chat).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -1544,7 +1399,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(out *jwriter.Writer, in RoomEventMessageData) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(out *jwriter.Writer, in RoomEventMessageData) { out.RawByte('{') first := true _ = first @@ -1564,27 +1419,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(out // MarshalJSON supports json.Marshaler interface func (v RoomEventMessageData) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomEventMessageData) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling10(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomEventMessageData) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomEventMessageData) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api10(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling10(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(in *jlexer.Lexer, out *RoomEventMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(in *jlexer.Lexer, out *RoomEventMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1597,20 +1452,17 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Data).UnmarshalJSON(data)) } default: in.SkipRecursive() @@ -1622,7 +1474,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(out *jwriter.Writer, in RoomEventMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(out *jwriter.Writer, in RoomEventMessage) { out.RawByte('{') first := true _ = first @@ -1642,27 +1494,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(out // MarshalJSON supports json.Marshaler interface func (v RoomEventMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomEventMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling11(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomEventMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomEventMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api11(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling11(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(in *jlexer.Lexer, out *RoomErrorDetails) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(in *jlexer.Lexer, out *RoomErrorDetails) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1675,6 +1527,11 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "room": if in.IsNull() { @@ -1684,11 +1541,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(in if out.Room == nil { out.Room = new(RoomServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Room).UnmarshalEasyJSON(in) - } + (*out.Room).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -1700,7 +1553,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(out *jwriter.Writer, in RoomErrorDetails) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(out *jwriter.Writer, in RoomErrorDetails) { out.RawByte('{') first := true _ = first @@ -1719,27 +1572,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(out // MarshalJSON supports json.Marshaler interface func (v RoomErrorDetails) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomErrorDetails) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling12(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomErrorDetails) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomErrorDetails) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api12(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling12(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(in *jlexer.Lexer, out *RoomDisinviteEventServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(in *jlexer.Lexer, out *RoomDisinviteEventServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1752,34 +1605,23 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "reason": - if in.IsNull() { - in.Skip() - } else { - out.Reason = string(in.String()) - } + out.Reason = string(in.String()) case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "properties": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Properties).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Properties).UnmarshalJSON(data)) } case "incall": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.InCall).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.InCall).UnmarshalJSON(data)) } case "changed": if in.IsNull() { @@ -1789,41 +1631,41 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(in in.Delim('[') if out.Changed == nil { if !in.IsDelim(']') { - out.Changed = make([]StringMap, 0, 8) + out.Changed = make([]map[string]interface{}, 0, 8) } else { - out.Changed = []StringMap{} + out.Changed = []map[string]interface{}{} } } else { out.Changed = (out.Changed)[:0] } for !in.IsDelim(']') { - var v19 StringMap + var v18 map[string]interface{} if in.IsNull() { in.Skip() } else { in.Delim('{') if !in.IsDelim('}') { - v19 = make(StringMap) + v18 = make(map[string]interface{}) } else { - v19 = nil + v18 = nil } for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v20 interface{} - if m, ok := v20.(easyjson.Unmarshaler); ok { + var v19 interface{} + if m, ok := v19.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) - } else if m, ok := v20.(json.Unmarshaler); ok { + } else if m, ok := v19.(json.Unmarshaler); ok { _ = m.UnmarshalJSON(in.Raw()) } else { - v20 = in.Interface() + v19 = in.Interface() } - (v19)[key] = v20 + (v18)[key] = v19 in.WantComma() } in.Delim('}') } - out.Changed = append(out.Changed, v19) + out.Changed = append(out.Changed, v18) in.WantComma() } in.Delim(']') @@ -1836,51 +1678,47 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(in in.Delim('[') if out.Users == nil { if !in.IsDelim(']') { - out.Users = make([]StringMap, 0, 8) + out.Users = make([]map[string]interface{}, 0, 8) } else { - out.Users = []StringMap{} + out.Users = []map[string]interface{}{} } } else { out.Users = (out.Users)[:0] } for !in.IsDelim(']') { - var v21 StringMap + var v20 map[string]interface{} if in.IsNull() { in.Skip() } else { in.Delim('{') if !in.IsDelim('}') { - v21 = make(StringMap) + v20 = make(map[string]interface{}) } else { - v21 = nil + v20 = nil } for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v22 interface{} - if m, ok := v22.(easyjson.Unmarshaler); ok { + var v21 interface{} + if m, ok := v21.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) - } else if m, ok := v22.(json.Unmarshaler); ok { + } else if m, ok := v21.(json.Unmarshaler); ok { _ = m.UnmarshalJSON(in.Raw()) } else { - v22 = in.Interface() + v21 = in.Interface() } - (v21)[key] = v22 + (v20)[key] = v21 in.WantComma() } in.Delim('}') } - out.Users = append(out.Users, v21) + out.Users = append(out.Users, v20) in.WantComma() } in.Delim(']') } case "all": - if in.IsNull() { - in.Skip() - } else { - out.All = bool(in.Bool()) - } + out.All = bool(in.Bool()) default: in.SkipRecursive() } @@ -1891,7 +1729,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(out *jwriter.Writer, in RoomDisinviteEventServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(out *jwriter.Writer, in RoomDisinviteEventServerMessage) { out.RawByte('{') first := true _ = first @@ -1920,29 +1758,29 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(out out.RawString(prefix) { out.RawByte('[') - for v23, v24 := range in.Changed { - if v23 > 0 { + for v22, v23 := range in.Changed { + if v22 > 0 { out.RawByte(',') } - if v24 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + if v23 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { out.RawString(`null`) } else { out.RawByte('{') - v25First := true - for v25Name, v25Value := range v24 { - if v25First { - v25First = false + v24First := true + for v24Name, v24Value := range v23 { + if v24First { + v24First = false } else { out.RawByte(',') } - out.String(string(v25Name)) + out.String(string(v24Name)) out.RawByte(':') - if m, ok := v25Value.(easyjson.Marshaler); ok { + if m, ok := v24Value.(easyjson.Marshaler); ok { m.MarshalEasyJSON(out) - } else if m, ok := v25Value.(json.Marshaler); ok { + } else if m, ok := v24Value.(json.Marshaler); ok { out.Raw(m.MarshalJSON()) } else { - out.Raw(json.Marshal(v25Value)) + out.Raw(json.Marshal(v24Value)) } } out.RawByte('}') @@ -1956,29 +1794,29 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(out out.RawString(prefix) { out.RawByte('[') - for v26, v27 := range in.Users { - if v26 > 0 { + for v25, v26 := range in.Users { + if v25 > 0 { out.RawByte(',') } - if v27 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + if v26 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { out.RawString(`null`) } else { out.RawByte('{') - v28First := true - for v28Name, v28Value := range v27 { - if v28First { - v28First = false + v27First := true + for v27Name, v27Value := range v26 { + if v27First { + v27First = false } else { out.RawByte(',') } - out.String(string(v28Name)) + out.String(string(v27Name)) out.RawByte(':') - if m, ok := v28Value.(easyjson.Marshaler); ok { + if m, ok := v27Value.(easyjson.Marshaler); ok { m.MarshalEasyJSON(out) - } else if m, ok := v28Value.(json.Marshaler); ok { + } else if m, ok := v27Value.(json.Marshaler); ok { out.Raw(m.MarshalJSON()) } else { - out.Raw(json.Marshal(v28Value)) + out.Raw(json.Marshal(v27Value)) } } out.RawByte('}') @@ -1998,27 +1836,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(out // MarshalJSON supports json.Marshaler interface func (v RoomDisinviteEventServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomDisinviteEventServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling13(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomDisinviteEventServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomDisinviteEventServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api13(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling13(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(in *jlexer.Lexer, out *RoomClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(in *jlexer.Lexer, out *RoomClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2031,19 +1869,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = RoomSessionId(in.String()) - } + out.SessionId = string(in.String()) case "federation": if in.IsNull() { in.Skip() @@ -2052,11 +1887,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(in if out.Federation == nil { out.Federation = new(RoomFederationMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Federation).UnmarshalEasyJSON(in) - } + (*out.Federation).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -2068,7 +1899,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(out *jwriter.Writer, in RoomClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(out *jwriter.Writer, in RoomClientMessage) { out.RawByte('{') first := true _ = first @@ -2093,27 +1924,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(out // MarshalJSON supports json.Marshaler interface func (v RoomClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RoomClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling14(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RoomClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RoomClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api14(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling14(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api15(in *jlexer.Lexer, out *RoomBandwidth) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(in *jlexer.Lexer, out *RemoveSessionInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2126,101 +1957,18 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api15(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() - switch key { - case "maxstreambitrate": - if in.IsNull() { - in.Skip() - } else { - out.MaxStreamBitrate = Bandwidth(in.Uint64()) - } - case "maxscreenbitrate": - if in.IsNull() { - in.Skip() - } else { - out.MaxScreenBitrate = Bandwidth(in.Uint64()) - } - default: - in.SkipRecursive() + if in.IsNull() { + in.Skip() + in.WantComma() + continue } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api15(out *jwriter.Writer, in RoomBandwidth) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"maxstreambitrate\":" - out.RawString(prefix[1:]) - out.Uint64(uint64(in.MaxStreamBitrate)) - } - { - const prefix string = ",\"maxscreenbitrate\":" - out.RawString(prefix) - out.Uint64(uint64(in.MaxScreenBitrate)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v RoomBandwidth) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api15(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v RoomBandwidth) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api15(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *RoomBandwidth) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api15(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *RoomBandwidth) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api15(l, v) -} -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api16(in *jlexer.Lexer, out *RemoveSessionInternalClientMessage) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() switch key { case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } + out.UserId = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) default: in.SkipRecursive() } @@ -2231,7 +1979,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api16(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api16(out *jwriter.Writer, in RemoveSessionInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(out *jwriter.Writer, in RemoveSessionInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -2262,27 +2010,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api16(out // MarshalJSON supports json.Marshaler interface func (v RemoveSessionInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api16(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v RemoveSessionInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api16(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling15(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *RemoveSessionInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api16(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *RemoveSessionInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api16(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling15(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(in *jlexer.Lexer, out *MessageServerMessageSender) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(in *jlexer.Lexer, out *MessageServerMessageSender) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2295,25 +2043,18 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } + out.UserId = string(in.String()) default: in.SkipRecursive() } @@ -2324,7 +2065,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(out *jwriter.Writer, in MessageServerMessageSender) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(out *jwriter.Writer, in MessageServerMessageSender) { out.RawByte('{') first := true _ = first @@ -2349,27 +2090,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(out // MarshalJSON supports json.Marshaler interface func (v MessageServerMessageSender) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageServerMessageSender) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling16(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageServerMessageSender) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageServerMessageSender) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api17(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling16(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(in *jlexer.Lexer, out *MessageServerMessageData) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(in *jlexer.Lexer, out *MessageServerMessageDataChat) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2382,12 +2123,89 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "refresh": + out.Refresh = bool(in.Bool()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(out *jwriter.Writer, in MessageServerMessageDataChat) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"refresh\":" + out.RawString(prefix[1:]) + out.Bool(bool(in.Refresh)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v MessageServerMessageDataChat) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v MessageServerMessageDataChat) MarshalEasyJSON(w *jwriter.Writer) { + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling17(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *MessageServerMessageDataChat) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *MessageServerMessageDataChat) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling17(l, v) +} +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(in *jlexer.Lexer, out *MessageServerMessageData) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": + out.Type = string(in.String()) + case "chat": if in.IsNull() { in.Skip() + out.Chat = nil } else { - out.Type = string(in.String()) + if out.Chat == nil { + out.Chat = new(MessageServerMessageDataChat) + } + (*out.Chat).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -2399,7 +2217,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(out *jwriter.Writer, in MessageServerMessageData) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(out *jwriter.Writer, in MessageServerMessageData) { out.RawByte('{') first := true _ = first @@ -2408,33 +2226,38 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(out out.RawString(prefix[1:]) out.String(string(in.Type)) } + if in.Chat != nil { + const prefix string = ",\"chat\":" + out.RawString(prefix) + (*in.Chat).MarshalEasyJSON(out) + } out.RawByte('}') } // MarshalJSON supports json.Marshaler interface func (v MessageServerMessageData) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageServerMessageData) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling18(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageServerMessageData) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageServerMessageData) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api18(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling18(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(in *jlexer.Lexer, out *MessageServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(in *jlexer.Lexer, out *MessageServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2447,6 +2270,11 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "sender": if in.IsNull() { @@ -2456,11 +2284,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(in if out.Sender == nil { out.Sender = new(MessageServerMessageSender) } - if in.IsNull() { - in.Skip() - } else { - (*out.Sender).UnmarshalEasyJSON(in) - } + (*out.Sender).UnmarshalEasyJSON(in) } case "recipient": if in.IsNull() { @@ -2470,19 +2294,11 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(in if out.Recipient == nil { out.Recipient = new(MessageClientMessageRecipient) } - if in.IsNull() { - in.Skip() - } else { - (*out.Recipient).UnmarshalEasyJSON(in) - } + (*out.Recipient).UnmarshalEasyJSON(in) } case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Data).UnmarshalJSON(data)) } default: in.SkipRecursive() @@ -2494,7 +2310,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(out *jwriter.Writer, in MessageServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(out *jwriter.Writer, in MessageServerMessage) { out.RawByte('{') first := true _ = first @@ -2523,27 +2339,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(out // MarshalJSON supports json.Marshaler interface func (v MessageServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling19(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api19(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling19(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(in *jlexer.Lexer, out *MessageClientMessageRecipient) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(in *jlexer.Lexer, out *MessageClientMessageRecipient) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2556,25 +2372,18 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } + out.UserId = string(in.String()) default: in.SkipRecursive() } @@ -2585,7 +2394,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(out *jwriter.Writer, in MessageClientMessageRecipient) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(out *jwriter.Writer, in MessageClientMessageRecipient) { out.RawByte('{') first := true _ = first @@ -2610,27 +2419,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(out // MarshalJSON supports json.Marshaler interface func (v MessageClientMessageRecipient) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageClientMessageRecipient) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling20(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageClientMessageRecipient) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageClientMessageRecipient) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api20(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling20(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(in *jlexer.Lexer, out *MessageClientMessageData) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(in *jlexer.Lexer, out *MessageClientMessageData) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2643,77 +2452,50 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "sid": - if in.IsNull() { - in.Skip() - } else { - out.Sid = string(in.String()) - } + out.Sid = string(in.String()) case "roomType": - if in.IsNull() { - in.Skip() - } else { - out.RoomType = string(in.String()) - } + out.RoomType = string(in.String()) case "payload": if in.IsNull() { in.Skip() } else { in.Delim('{') - out.Payload = make(StringMap) + out.Payload = make(map[string]interface{}) for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v29 interface{} - if m, ok := v29.(easyjson.Unmarshaler); ok { + var v28 interface{} + if m, ok := v28.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) - } else if m, ok := v29.(json.Unmarshaler); ok { + } else if m, ok := v28.(json.Unmarshaler); ok { _ = m.UnmarshalJSON(in.Raw()) } else { - v29 = in.Interface() + v28 = in.Interface() } - (out.Payload)[key] = v29 + (out.Payload)[key] = v28 in.WantComma() } in.Delim('}') } case "bitrate": - if in.IsNull() { - in.Skip() - } else { - out.Bitrate = Bandwidth(in.Uint64()) - } + out.Bitrate = int(in.Int()) case "audiocodec": - if in.IsNull() { - in.Skip() - } else { - out.AudioCodec = string(in.String()) - } + out.AudioCodec = string(in.String()) case "videocodec": - if in.IsNull() { - in.Skip() - } else { - out.VideoCodec = string(in.String()) - } + out.VideoCodec = string(in.String()) case "vp9profile": - if in.IsNull() { - in.Skip() - } else { - out.VP9Profile = string(in.String()) - } + out.VP9Profile = string(in.String()) case "h264profile": - if in.IsNull() { - in.Skip() - } else { - out.H264Profile = string(in.String()) - } + out.H264Profile = string(in.String()) default: in.SkipRecursive() } @@ -2724,7 +2506,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(out *jwriter.Writer, in MessageClientMessageData) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(out *jwriter.Writer, in MessageClientMessageData) { out.RawByte('{') first := true _ = first @@ -2750,21 +2532,21 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(out out.RawString(`null`) } else { out.RawByte('{') - v30First := true - for v30Name, v30Value := range in.Payload { - if v30First { - v30First = false + v29First := true + for v29Name, v29Value := range in.Payload { + if v29First { + v29First = false } else { out.RawByte(',') } - out.String(string(v30Name)) + out.String(string(v29Name)) out.RawByte(':') - if m, ok := v30Value.(easyjson.Marshaler); ok { + if m, ok := v29Value.(easyjson.Marshaler); ok { m.MarshalEasyJSON(out) - } else if m, ok := v30Value.(json.Marshaler); ok { + } else if m, ok := v29Value.(json.Marshaler); ok { out.Raw(m.MarshalJSON()) } else { - out.Raw(json.Marshal(v30Value)) + out.Raw(json.Marshal(v29Value)) } } out.RawByte('}') @@ -2773,7 +2555,7 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(out if in.Bitrate != 0 { const prefix string = ",\"bitrate\":" out.RawString(prefix) - out.Uint64(uint64(in.Bitrate)) + out.Int(int(in.Bitrate)) } if in.AudioCodec != "" { const prefix string = ",\"audiocodec\":" @@ -2801,27 +2583,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(out // MarshalJSON supports json.Marshaler interface func (v MessageClientMessageData) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageClientMessageData) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling21(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageClientMessageData) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageClientMessageData) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api21(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling21(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(in *jlexer.Lexer, out *MessageClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(in *jlexer.Lexer, out *MessageClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2834,20 +2616,17 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "recipient": - if in.IsNull() { - in.Skip() - } else { - (out.Recipient).UnmarshalEasyJSON(in) - } + (out.Recipient).UnmarshalEasyJSON(in) case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Data).UnmarshalJSON(data)) } default: in.SkipRecursive() @@ -2859,7 +2638,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(out *jwriter.Writer, in MessageClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(out *jwriter.Writer, in MessageClientMessage) { out.RawByte('{') first := true _ = first @@ -2879,27 +2658,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(out // MarshalJSON supports json.Marshaler interface func (v MessageClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v MessageClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling22(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *MessageClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *MessageClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api22(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling22(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(in *jlexer.Lexer, out *InternalServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(in *jlexer.Lexer, out *InternalServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2912,13 +2691,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "dialout": if in.IsNull() { in.Skip() @@ -2927,11 +2707,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(in if out.Dialout == nil { out.Dialout = new(InternalServerDialoutRequest) } - if in.IsNull() { - in.Skip() - } else { - (*out.Dialout).UnmarshalEasyJSON(in) - } + (*out.Dialout).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -2943,7 +2719,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(out *jwriter.Writer, in InternalServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(out *jwriter.Writer, in InternalServerMessage) { out.RawByte('{') first := true _ = first @@ -2963,27 +2739,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(out // MarshalJSON supports json.Marshaler interface func (v InternalServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v InternalServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling23(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *InternalServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *InternalServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api23(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling23(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api24(in *jlexer.Lexer, out *InternalServerDialoutRequestContents) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(in *jlexer.Lexer, out *InternalServerDialoutRequest) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2996,110 +2772,25 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api24(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() - switch key { - case "number": - if in.IsNull() { - in.Skip() - } else { - out.Number = string(in.String()) - } - case "options": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Options).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() + if in.IsNull() { + in.Skip() + in.WantComma() + continue } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api24(out *jwriter.Writer, in InternalServerDialoutRequestContents) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"number\":" - out.RawString(prefix[1:]) - out.String(string(in.Number)) - } - if len(in.Options) != 0 { - const prefix string = ",\"options\":" - out.RawString(prefix) - out.Raw((in.Options).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v InternalServerDialoutRequestContents) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api24(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v InternalServerDialoutRequestContents) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api24(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *InternalServerDialoutRequestContents) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api24(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *InternalServerDialoutRequestContents) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api24(l, v) -} -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api25(in *jlexer.Lexer, out *InternalServerDialoutRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() switch key { case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "backend": - if in.IsNull() { - in.Skip() - } else { - out.Backend = string(in.String()) - } + out.Backend = string(in.String()) case "request": if in.IsNull() { in.Skip() out.Request = nil } else { if out.Request == nil { - out.Request = new(InternalServerDialoutRequestContents) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Request).UnmarshalEasyJSON(in) + out.Request = new(BackendRoomDialoutRequest) } + (*out.Request).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -3111,7 +2802,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api25(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api25(out *jwriter.Writer, in InternalServerDialoutRequest) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(out *jwriter.Writer, in InternalServerDialoutRequest) { out.RawByte('{') first := true _ = first @@ -3140,27 +2831,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api25(out // MarshalJSON supports json.Marshaler interface func (v InternalServerDialoutRequest) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api25(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v InternalServerDialoutRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api25(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling24(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *InternalServerDialoutRequest) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api25(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *InternalServerDialoutRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api25(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling24(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(in *jlexer.Lexer, out *InternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(in *jlexer.Lexer, out *InternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3173,13 +2864,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "addsession": if in.IsNull() { in.Skip() @@ -3188,11 +2880,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(in if out.AddSession == nil { out.AddSession = new(AddSessionInternalClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.AddSession).UnmarshalEasyJSON(in) - } + (*out.AddSession).UnmarshalEasyJSON(in) } case "updatesession": if in.IsNull() { @@ -3202,11 +2890,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(in if out.UpdateSession == nil { out.UpdateSession = new(UpdateSessionInternalClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.UpdateSession).UnmarshalEasyJSON(in) - } + (*out.UpdateSession).UnmarshalEasyJSON(in) } case "removesession": if in.IsNull() { @@ -3216,11 +2900,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(in if out.RemoveSession == nil { out.RemoveSession = new(RemoveSessionInternalClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.RemoveSession).UnmarshalEasyJSON(in) - } + (*out.RemoveSession).UnmarshalEasyJSON(in) } case "incall": if in.IsNull() { @@ -3230,11 +2910,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(in if out.InCall == nil { out.InCall = new(InCallInternalClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.InCall).UnmarshalEasyJSON(in) - } + (*out.InCall).UnmarshalEasyJSON(in) } case "dialout": if in.IsNull() { @@ -3244,11 +2920,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(in if out.Dialout == nil { out.Dialout = new(DialoutInternalClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Dialout).UnmarshalEasyJSON(in) - } + (*out.Dialout).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -3260,7 +2932,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(out *jwriter.Writer, in InternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(out *jwriter.Writer, in InternalClientMessage) { out.RawByte('{') first := true _ = first @@ -3300,27 +2972,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(out // MarshalJSON supports json.Marshaler interface func (v InternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v InternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling25(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *InternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *InternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api26(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling25(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(in *jlexer.Lexer, out *InCallInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(in *jlexer.Lexer, out *InCallInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3333,13 +3005,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "incall": - if in.IsNull() { - in.Skip() - } else { - out.InCall = int(in.Int()) - } + out.InCall = int(in.Int()) default: in.SkipRecursive() } @@ -3350,7 +3023,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(out *jwriter.Writer, in InCallInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(out *jwriter.Writer, in InCallInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -3365,27 +3038,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(out // MarshalJSON supports json.Marshaler interface func (v InCallInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v InCallInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling26(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *InCallInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *InCallInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api27(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling26(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(in *jlexer.Lexer, out *HelloV2TokenClaims) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(in *jlexer.Lexer, out *HelloV2TokenClaims) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3398,34 +3071,23 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "userdata": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.UserData).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.UserData).UnmarshalJSON(data)) } case "iss": - if in.IsNull() { - in.Skip() - } else { - out.Issuer = string(in.String()) - } + out.Issuer = string(in.String()) case "sub": - if in.IsNull() { - in.Skip() - } else { - out.Subject = string(in.String()) - } + out.Subject = string(in.String()) case "aud": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Audience).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Audience).UnmarshalJSON(data)) } case "exp": if in.IsNull() { @@ -3435,12 +3097,8 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(in if out.ExpiresAt == nil { out.ExpiresAt = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.ExpiresAt).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.ExpiresAt).UnmarshalJSON(data)) } } case "nbf": @@ -3451,12 +3109,8 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(in if out.NotBefore == nil { out.NotBefore = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.NotBefore).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.NotBefore).UnmarshalJSON(data)) } } case "iat": @@ -3467,20 +3121,12 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(in if out.IssuedAt == nil { out.IssuedAt = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.IssuedAt).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.IssuedAt).UnmarshalJSON(data)) } } case "jti": - if in.IsNull() { - in.Skip() - } else { - out.ID = string(in.String()) - } + out.ID = string(in.String()) default: in.SkipRecursive() } @@ -3491,7 +3137,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(out *jwriter.Writer, in HelloV2TokenClaims) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(out *jwriter.Writer, in HelloV2TokenClaims) { out.RawByte('{') first := true _ = first @@ -3577,27 +3223,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(out // MarshalJSON supports json.Marshaler interface func (v HelloV2TokenClaims) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloV2TokenClaims) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling27(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloV2TokenClaims) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloV2TokenClaims) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api28(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling27(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(in *jlexer.Lexer, out *HelloV2AuthParams) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(in *jlexer.Lexer, out *HelloV2AuthParams) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3610,13 +3256,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "token": - if in.IsNull() { - in.Skip() - } else { - out.Token = string(in.String()) - } + out.Token = string(in.String()) default: in.SkipRecursive() } @@ -3627,7 +3274,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(out *jwriter.Writer, in HelloV2AuthParams) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(out *jwriter.Writer, in HelloV2AuthParams) { out.RawByte('{') first := true _ = first @@ -3642,27 +3289,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(out // MarshalJSON supports json.Marshaler interface func (v HelloV2AuthParams) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloV2AuthParams) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling28(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloV2AuthParams) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloV2AuthParams) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api29(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling28(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(in *jlexer.Lexer, out *HelloServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(in *jlexer.Lexer, out *HelloServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3675,31 +3322,20 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } + out.Version = string(in.String()) case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "resumeid": - if in.IsNull() { - in.Skip() - } else { - out.ResumeId = PrivateSessionId(in.String()) - } + out.ResumeId = string(in.String()) case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } + out.UserId = string(in.String()) case "server": if in.IsNull() { in.Skip() @@ -3708,11 +3344,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(in if out.Server == nil { out.Server = new(WelcomeServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Server).UnmarshalEasyJSON(in) - } + (*out.Server).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -3724,7 +3356,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(out *jwriter.Writer, in HelloServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(out *jwriter.Writer, in HelloServerMessage) { out.RawByte('{') first := true _ = first @@ -3759,27 +3391,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(out // MarshalJSON supports json.Marshaler interface func (v HelloServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling29(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api30(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling29(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(in *jlexer.Lexer, out *HelloClientMessageAuth) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(in *jlexer.Lexer, out *HelloClientMessageAuth) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3792,27 +3424,20 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = ClientType(in.String()) - } + out.Type = string(in.String()) case "params": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Params).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Params).UnmarshalJSON(data)) } case "url": - if in.IsNull() { - in.Skip() - } else { - out.Url = string(in.String()) - } + out.Url = string(in.String()) default: in.SkipRecursive() } @@ -3823,7 +3448,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(out *jwriter.Writer, in HelloClientMessageAuth) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(out *jwriter.Writer, in HelloClientMessageAuth) { out.RawByte('{') first := true _ = first @@ -3854,27 +3479,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(out // MarshalJSON supports json.Marshaler interface func (v HelloClientMessageAuth) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloClientMessageAuth) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling30(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloClientMessageAuth) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloClientMessageAuth) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api31(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling30(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(in *jlexer.Lexer, out *HelloClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(in *jlexer.Lexer, out *HelloClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -3887,19 +3512,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } + out.Version = string(in.String()) case "resumeid": - if in.IsNull() { - in.Skip() - } else { - out.ResumeId = PrivateSessionId(in.String()) - } + out.ResumeId = string(in.String()) case "features": if in.IsNull() { in.Skip() @@ -3916,13 +3538,9 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(in out.Features = (out.Features)[:0] } for !in.IsDelim(']') { - var v31 string - if in.IsNull() { - in.Skip() - } else { - v31 = string(in.String()) - } - out.Features = append(out.Features, v31) + var v30 string + v30 = string(in.String()) + out.Features = append(out.Features, v30) in.WantComma() } in.Delim(']') @@ -3935,11 +3553,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(in if out.Auth == nil { out.Auth = new(HelloClientMessageAuth) } - if in.IsNull() { - in.Skip() - } else { - (*out.Auth).UnmarshalEasyJSON(in) - } + (*out.Auth).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -3951,7 +3565,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(out *jwriter.Writer, in HelloClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(out *jwriter.Writer, in HelloClientMessage) { out.RawByte('{') first := true _ = first @@ -3970,11 +3584,11 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(out out.RawString(prefix) { out.RawByte('[') - for v32, v33 := range in.Features { - if v32 > 0 { + for v31, v32 := range in.Features { + if v31 > 0 { out.RawByte(',') } - out.String(string(v33)) + out.String(string(v32)) } out.RawByte(']') } @@ -3990,27 +3604,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(out // MarshalJSON supports json.Marshaler interface func (v HelloClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v HelloClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling31(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *HelloClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *HelloClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api32(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling31(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(in *jlexer.Lexer, out *FederationTokenClaims) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(in *jlexer.Lexer, out *FederationTokenClaims) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4023,34 +3637,23 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "userdata": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.UserData).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.UserData).UnmarshalJSON(data)) } case "iss": - if in.IsNull() { - in.Skip() - } else { - out.Issuer = string(in.String()) - } + out.Issuer = string(in.String()) case "sub": - if in.IsNull() { - in.Skip() - } else { - out.Subject = string(in.String()) - } + out.Subject = string(in.String()) case "aud": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Audience).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Audience).UnmarshalJSON(data)) } case "exp": if in.IsNull() { @@ -4060,12 +3663,8 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(in if out.ExpiresAt == nil { out.ExpiresAt = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.ExpiresAt).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.ExpiresAt).UnmarshalJSON(data)) } } case "nbf": @@ -4076,12 +3675,8 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(in if out.NotBefore == nil { out.NotBefore = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.NotBefore).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.NotBefore).UnmarshalJSON(data)) } } case "iat": @@ -4092,20 +3687,12 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(in if out.IssuedAt == nil { out.IssuedAt = new(_v5.NumericDate) } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.IssuedAt).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((*out.IssuedAt).UnmarshalJSON(data)) } } case "jti": - if in.IsNull() { - in.Skip() - } else { - out.ID = string(in.String()) - } + out.ID = string(in.String()) default: in.SkipRecursive() } @@ -4116,7 +3703,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(out *jwriter.Writer, in FederationTokenClaims) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(out *jwriter.Writer, in FederationTokenClaims) { out.RawByte('{') first := true _ = first @@ -4202,27 +3789,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(out // MarshalJSON supports json.Marshaler interface func (v FederationTokenClaims) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v FederationTokenClaims) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling32(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *FederationTokenClaims) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *FederationTokenClaims) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api33(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling32(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(in *jlexer.Lexer, out *FederationAuthParams) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(in *jlexer.Lexer, out *FederationAuthParams) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4235,13 +3822,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "token": - if in.IsNull() { - in.Skip() - } else { - out.Token = string(in.String()) - } + out.Token = string(in.String()) default: in.SkipRecursive() } @@ -4252,7 +3840,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(out *jwriter.Writer, in FederationAuthParams) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(out *jwriter.Writer, in FederationAuthParams) { out.RawByte('{') first := true _ = first @@ -4267,27 +3855,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(out // MarshalJSON supports json.Marshaler interface func (v FederationAuthParams) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v FederationAuthParams) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling33(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *FederationAuthParams) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *FederationAuthParams) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api34(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling33(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(in *jlexer.Lexer, out *EventServerMessageSwitchTo) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(in *jlexer.Lexer, out *EventServerMessageSwitchTo) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4300,20 +3888,17 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "details": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Details).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Details).UnmarshalJSON(data)) } default: in.SkipRecursive() @@ -4325,7 +3910,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(out *jwriter.Writer, in EventServerMessageSwitchTo) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(out *jwriter.Writer, in EventServerMessageSwitchTo) { out.RawByte('{') first := true _ = first @@ -4345,27 +3930,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(out // MarshalJSON supports json.Marshaler interface func (v EventServerMessageSwitchTo) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventServerMessageSwitchTo) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling34(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EventServerMessageSwitchTo) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventServerMessageSwitchTo) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api35(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling34(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(in *jlexer.Lexer, out *EventServerMessageSessionEntry) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(in *jlexer.Lexer, out *EventServerMessageSessionEntry) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4378,19 +3963,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } + out.UserId = string(in.String()) case "features": if in.IsNull() { in.Skip() @@ -4407,37 +3989,21 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(in out.Features = (out.Features)[:0] } for !in.IsDelim(']') { - var v34 string - if in.IsNull() { - in.Skip() - } else { - v34 = string(in.String()) - } - out.Features = append(out.Features, v34) + var v33 string + v33 = string(in.String()) + out.Features = append(out.Features, v33) in.WantComma() } in.Delim(']') } case "user": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.User).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.User).UnmarshalJSON(data)) } case "roomsessionid": - if in.IsNull() { - in.Skip() - } else { - out.RoomSessionId = RoomSessionId(in.String()) - } + out.RoomSessionId = string(in.String()) case "federated": - if in.IsNull() { - in.Skip() - } else { - out.Federated = bool(in.Bool()) - } + out.Federated = bool(in.Bool()) default: in.SkipRecursive() } @@ -4448,7 +4014,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(out *jwriter.Writer, in EventServerMessageSessionEntry) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(out *jwriter.Writer, in EventServerMessageSessionEntry) { out.RawByte('{') first := true _ = first @@ -4467,11 +4033,11 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(out out.RawString(prefix) { out.RawByte('[') - for v35, v36 := range in.Features { - if v35 > 0 { + for v34, v35 := range in.Features { + if v34 > 0 { out.RawByte(',') } - out.String(string(v36)) + out.String(string(v35)) } out.RawByte(']') } @@ -4497,27 +4063,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(out // MarshalJSON supports json.Marshaler interface func (v EventServerMessageSessionEntry) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventServerMessageSessionEntry) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling35(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EventServerMessageSessionEntry) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventServerMessageSessionEntry) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api36(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling35(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in *jlexer.Lexer, out *EventServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(in *jlexer.Lexer, out *EventServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4530,19 +4096,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "target": - if in.IsNull() { - in.Skip() - } else { - out.Target = string(in.String()) - } + out.Target = string(in.String()) case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "join": if in.IsNull() { in.Skip() @@ -4551,21 +4114,25 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in in.Delim('[') if out.Join == nil { if !in.IsDelim(']') { - out.Join = make([]EventServerMessageSessionEntry, 0, 0) + out.Join = make([]*EventServerMessageSessionEntry, 0, 8) } else { - out.Join = []EventServerMessageSessionEntry{} + out.Join = []*EventServerMessageSessionEntry{} } } else { out.Join = (out.Join)[:0] } for !in.IsDelim(']') { - var v37 EventServerMessageSessionEntry + var v36 *EventServerMessageSessionEntry if in.IsNull() { in.Skip() + v36 = nil } else { - (v37).UnmarshalEasyJSON(in) + if v36 == nil { + v36 = new(EventServerMessageSessionEntry) + } + (*v36).UnmarshalEasyJSON(in) } - out.Join = append(out.Join, v37) + out.Join = append(out.Join, v36) in.WantComma() } in.Delim(']') @@ -4578,21 +4145,17 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in in.Delim('[') if out.Leave == nil { if !in.IsDelim(']') { - out.Leave = make([]PublicSessionId, 0, 4) + out.Leave = make([]string, 0, 4) } else { - out.Leave = []PublicSessionId{} + out.Leave = []string{} } } else { out.Leave = (out.Leave)[:0] } for !in.IsDelim(']') { - var v38 PublicSessionId - if in.IsNull() { - in.Skip() - } else { - v38 = PublicSessionId(in.String()) - } - out.Leave = append(out.Leave, v38) + var v37 string + v37 = string(in.String()) + out.Leave = append(out.Leave, v37) in.WantComma() } in.Delim(']') @@ -4605,21 +4168,25 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in in.Delim('[') if out.Change == nil { if !in.IsDelim(']') { - out.Change = make([]EventServerMessageSessionEntry, 0, 0) + out.Change = make([]*EventServerMessageSessionEntry, 0, 8) } else { - out.Change = []EventServerMessageSessionEntry{} + out.Change = []*EventServerMessageSessionEntry{} } } else { out.Change = (out.Change)[:0] } for !in.IsDelim(']') { - var v39 EventServerMessageSessionEntry + var v38 *EventServerMessageSessionEntry if in.IsNull() { in.Skip() + v38 = nil } else { - (v39).UnmarshalEasyJSON(in) + if v38 == nil { + v38 = new(EventServerMessageSessionEntry) + } + (*v38).UnmarshalEasyJSON(in) } - out.Change = append(out.Change, v39) + out.Change = append(out.Change, v38) in.WantComma() } in.Delim(']') @@ -4632,11 +4199,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in if out.SwitchTo == nil { out.SwitchTo = new(EventServerMessageSwitchTo) } - if in.IsNull() { - in.Skip() - } else { - (*out.SwitchTo).UnmarshalEasyJSON(in) - } + (*out.SwitchTo).UnmarshalEasyJSON(in) } case "resumed": if in.IsNull() { @@ -4646,11 +4209,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in if out.Resumed == nil { out.Resumed = new(bool) } - if in.IsNull() { - in.Skip() - } else { - *out.Resumed = bool(in.Bool()) - } + *out.Resumed = bool(in.Bool()) } case "invite": if in.IsNull() { @@ -4660,11 +4219,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in if out.Invite == nil { out.Invite = new(RoomEventServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Invite).UnmarshalEasyJSON(in) - } + (*out.Invite).UnmarshalEasyJSON(in) } case "disinvite": if in.IsNull() { @@ -4674,11 +4229,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in if out.Disinvite == nil { out.Disinvite = new(RoomDisinviteEventServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Disinvite).UnmarshalEasyJSON(in) - } + (*out.Disinvite).UnmarshalEasyJSON(in) } case "update": if in.IsNull() { @@ -4688,11 +4239,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in if out.Update == nil { out.Update = new(RoomEventServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Update).UnmarshalEasyJSON(in) - } + (*out.Update).UnmarshalEasyJSON(in) } case "flags": if in.IsNull() { @@ -4702,11 +4249,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in if out.Flags == nil { out.Flags = new(RoomFlagsServerMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Flags).UnmarshalEasyJSON(in) - } + (*out.Flags).UnmarshalEasyJSON(in) } case "message": if in.IsNull() { @@ -4716,11 +4259,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in if out.Message == nil { out.Message = new(RoomEventMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Message).UnmarshalEasyJSON(in) - } + (*out.Message).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -4732,7 +4271,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(out *jwriter.Writer, in EventServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(out *jwriter.Writer, in EventServerMessage) { out.RawByte('{') first := true _ = first @@ -4751,11 +4290,15 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(out out.RawString(prefix) { out.RawByte('[') - for v40, v41 := range in.Join { - if v40 > 0 { + for v39, v40 := range in.Join { + if v39 > 0 { out.RawByte(',') } - (v41).MarshalEasyJSON(out) + if v40 == nil { + out.RawString("null") + } else { + (*v40).MarshalEasyJSON(out) + } } out.RawByte(']') } @@ -4765,11 +4308,11 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(out out.RawString(prefix) { out.RawByte('[') - for v42, v43 := range in.Leave { - if v42 > 0 { + for v41, v42 := range in.Leave { + if v41 > 0 { out.RawByte(',') } - out.String(string(v43)) + out.String(string(v42)) } out.RawByte(']') } @@ -4779,11 +4322,15 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(out out.RawString(prefix) { out.RawByte('[') - for v44, v45 := range in.Change { - if v44 > 0 { + for v43, v44 := range in.Change { + if v43 > 0 { out.RawByte(',') } - (v45).MarshalEasyJSON(out) + if v44 == nil { + out.RawString("null") + } else { + (*v44).MarshalEasyJSON(out) + } } out.RawByte(']') } @@ -4829,27 +4376,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(out // MarshalJSON supports json.Marshaler interface func (v EventServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling36(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EventServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api37(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling36(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(in *jlexer.Lexer, out *Error) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(in *jlexer.Lexer, out *Error) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4862,26 +4409,19 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "code": - if in.IsNull() { - in.Skip() - } else { - out.Code = string(in.String()) - } + out.Code = string(in.String()) case "message": - if in.IsNull() { - in.Skip() - } else { - out.Message = string(in.String()) - } + out.Message = string(in.String()) case "details": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Details).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Details).UnmarshalJSON(data)) } default: in.SkipRecursive() @@ -4893,7 +4433,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(out *jwriter.Writer, in Error) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(out *jwriter.Writer, in Error) { out.RawByte('{') first := true _ = first @@ -4918,27 +4458,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(out // MarshalJSON supports json.Marshaler interface func (v Error) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v Error) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling37(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *Error) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *Error) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api38(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling37(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(in *jlexer.Lexer, out *DialoutStatusInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(in *jlexer.Lexer, out *DialoutStatusInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -4951,37 +4491,22 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "callid": - if in.IsNull() { - in.Skip() - } else { - out.CallId = string(in.String()) - } + out.CallId = string(in.String()) case "status": - if in.IsNull() { - in.Skip() - } else { - out.Status = DialoutStatus(in.String()) - } + out.Status = DialoutStatus(in.String()) case "cause": - if in.IsNull() { - in.Skip() - } else { - out.Cause = string(in.String()) - } + out.Cause = string(in.String()) case "code": - if in.IsNull() { - in.Skip() - } else { - out.Code = int(in.Int()) - } + out.Code = int(in.Int()) case "message": - if in.IsNull() { - in.Skip() - } else { - out.Message = string(in.String()) - } + out.Message = string(in.String()) default: in.SkipRecursive() } @@ -4992,7 +4517,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(out *jwriter.Writer, in DialoutStatusInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(out *jwriter.Writer, in DialoutStatusInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -5027,27 +4552,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(out // MarshalJSON supports json.Marshaler interface func (v DialoutStatusInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v DialoutStatusInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling38(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *DialoutStatusInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *DialoutStatusInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api39(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling38(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(in *jlexer.Lexer, out *DialoutInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(in *jlexer.Lexer, out *DialoutInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5060,19 +4585,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) case "error": if in.IsNull() { in.Skip() @@ -5081,11 +4603,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(in if out.Error == nil { out.Error = new(Error) } - if in.IsNull() { - in.Skip() - } else { - (*out.Error).UnmarshalEasyJSON(in) - } + (*out.Error).UnmarshalEasyJSON(in) } case "status": if in.IsNull() { @@ -5095,11 +4613,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(in if out.Status == nil { out.Status = new(DialoutStatusInternalClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Status).UnmarshalEasyJSON(in) - } + (*out.Status).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -5111,7 +4625,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(out *jwriter.Writer, in DialoutInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(out *jwriter.Writer, in DialoutInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -5141,27 +4655,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(out // MarshalJSON supports json.Marshaler interface func (v DialoutInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v DialoutInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling39(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *DialoutInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *DialoutInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api40(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling39(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(in *jlexer.Lexer, out *ControlServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(in *jlexer.Lexer, out *ControlServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5174,6 +4688,11 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "sender": if in.IsNull() { @@ -5183,11 +4702,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(in if out.Sender == nil { out.Sender = new(MessageServerMessageSender) } - if in.IsNull() { - in.Skip() - } else { - (*out.Sender).UnmarshalEasyJSON(in) - } + (*out.Sender).UnmarshalEasyJSON(in) } case "recipient": if in.IsNull() { @@ -5197,19 +4712,11 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(in if out.Recipient == nil { out.Recipient = new(MessageClientMessageRecipient) } - if in.IsNull() { - in.Skip() - } else { - (*out.Recipient).UnmarshalEasyJSON(in) - } + (*out.Recipient).UnmarshalEasyJSON(in) } case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Data).UnmarshalJSON(data)) } default: in.SkipRecursive() @@ -5221,7 +4728,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(out *jwriter.Writer, in ControlServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(out *jwriter.Writer, in ControlServerMessage) { out.RawByte('{') first := true _ = first @@ -5250,27 +4757,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(out // MarshalJSON supports json.Marshaler interface func (v ControlServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ControlServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling40(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ControlServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ControlServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api41(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling40(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(in *jlexer.Lexer, out *ControlClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(in *jlexer.Lexer, out *ControlClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5283,20 +4790,17 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "recipient": - if in.IsNull() { - in.Skip() - } else { - (out.Recipient).UnmarshalEasyJSON(in) - } + (out.Recipient).UnmarshalEasyJSON(in) case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.Data).UnmarshalJSON(data)) } default: in.SkipRecursive() @@ -5308,7 +4812,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(out *jwriter.Writer, in ControlClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(out *jwriter.Writer, in ControlClientMessage) { out.RawByte('{') first := true _ = first @@ -5328,27 +4832,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(out // MarshalJSON supports json.Marshaler interface func (v ControlClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ControlClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling41(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ControlClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ControlClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api42(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling41(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(in *jlexer.Lexer, out *CommonSessionInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(in *jlexer.Lexer, out *CommonSessionInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5361,19 +4865,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) default: in.SkipRecursive() } @@ -5384,7 +4885,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(out *jwriter.Writer, in CommonSessionInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(out *jwriter.Writer, in CommonSessionInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -5404,27 +4905,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(out // MarshalJSON supports json.Marshaler interface func (v CommonSessionInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v CommonSessionInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling42(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *CommonSessionInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *CommonSessionInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api43(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling42(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(in *jlexer.Lexer, out *ClientTypeInternalAuthParams) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(in *jlexer.Lexer, out *ClientTypeInternalAuthParams) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5437,25 +4938,18 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "random": - if in.IsNull() { - in.Skip() - } else { - out.Random = string(in.String()) - } + out.Random = string(in.String()) case "token": - if in.IsNull() { - in.Skip() - } else { - out.Token = string(in.String()) - } + out.Token = string(in.String()) case "backend": - if in.IsNull() { - in.Skip() - } else { - out.Backend = string(in.String()) - } + out.Backend = string(in.String()) default: in.SkipRecursive() } @@ -5466,7 +4960,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(out *jwriter.Writer, in ClientTypeInternalAuthParams) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(out *jwriter.Writer, in ClientTypeInternalAuthParams) { out.RawByte('{') first := true _ = first @@ -5491,27 +4985,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(out // MarshalJSON supports json.Marshaler interface func (v ClientTypeInternalAuthParams) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ClientTypeInternalAuthParams) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling43(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ClientTypeInternalAuthParams) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ClientTypeInternalAuthParams) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api44(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling43(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in *jlexer.Lexer, out *ClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(in *jlexer.Lexer, out *ClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5524,19 +5018,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "id": - if in.IsNull() { - in.Skip() - } else { - out.Id = string(in.String()) - } + out.Id = string(in.String()) case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "hello": if in.IsNull() { in.Skip() @@ -5545,11 +5036,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in if out.Hello == nil { out.Hello = new(HelloClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Hello).UnmarshalEasyJSON(in) - } + (*out.Hello).UnmarshalEasyJSON(in) } case "bye": if in.IsNull() { @@ -5559,11 +5046,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in if out.Bye == nil { out.Bye = new(ByeClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Bye).UnmarshalEasyJSON(in) - } + (*out.Bye).UnmarshalEasyJSON(in) } case "room": if in.IsNull() { @@ -5573,11 +5056,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in if out.Room == nil { out.Room = new(RoomClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Room).UnmarshalEasyJSON(in) - } + (*out.Room).UnmarshalEasyJSON(in) } case "message": if in.IsNull() { @@ -5587,11 +5066,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in if out.Message == nil { out.Message = new(MessageClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Message).UnmarshalEasyJSON(in) - } + (*out.Message).UnmarshalEasyJSON(in) } case "control": if in.IsNull() { @@ -5601,11 +5076,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in if out.Control == nil { out.Control = new(ControlClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Control).UnmarshalEasyJSON(in) - } + (*out.Control).UnmarshalEasyJSON(in) } case "internal": if in.IsNull() { @@ -5615,11 +5086,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in if out.Internal == nil { out.Internal = new(InternalClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.Internal).UnmarshalEasyJSON(in) - } + (*out.Internal).UnmarshalEasyJSON(in) } case "transient": if in.IsNull() { @@ -5629,11 +5096,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in if out.TransientData == nil { out.TransientData = new(TransientDataClientMessage) } - if in.IsNull() { - in.Skip() - } else { - (*out.TransientData).UnmarshalEasyJSON(in) - } + (*out.TransientData).UnmarshalEasyJSON(in) } default: in.SkipRecursive() @@ -5645,7 +5108,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(out *jwriter.Writer, in ClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(out *jwriter.Writer, in ClientMessage) { out.RawByte('{') first := true _ = first @@ -5706,27 +5169,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(out // MarshalJSON supports json.Marshaler interface func (v ClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling44(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api45(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling44(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(in *jlexer.Lexer, out *ByeServerMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(in *jlexer.Lexer, out *ByeServerMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5739,13 +5202,14 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "reason": - if in.IsNull() { - in.Skip() - } else { - out.Reason = string(in.String()) - } + out.Reason = string(in.String()) default: in.SkipRecursive() } @@ -5756,7 +5220,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(out *jwriter.Writer, in ByeServerMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(out *jwriter.Writer, in ByeServerMessage) { out.RawByte('{') first := true _ = first @@ -5771,27 +5235,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(out // MarshalJSON supports json.Marshaler interface func (v ByeServerMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ByeServerMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling45(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ByeServerMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ByeServerMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api46(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling45(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(in *jlexer.Lexer, out *ByeClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(in *jlexer.Lexer, out *ByeClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5804,6 +5268,11 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { default: in.SkipRecursive() @@ -5815,7 +5284,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(out *jwriter.Writer, in ByeClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(out *jwriter.Writer, in ByeClientMessage) { out.RawByte('{') first := true _ = first @@ -5825,27 +5294,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(out // MarshalJSON supports json.Marshaler interface func (v ByeClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v ByeClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling46(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *ByeClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ByeClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api47(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling46(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(in *jlexer.Lexer, out *AnswerOfferMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling47(in *jlexer.Lexer, out *AnswerOfferMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -5858,59 +5327,44 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "to": - if in.IsNull() { - in.Skip() - } else { - out.To = PublicSessionId(in.String()) - } + out.To = string(in.String()) case "from": - if in.IsNull() { - in.Skip() - } else { - out.From = PublicSessionId(in.String()) - } + out.From = string(in.String()) case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } + out.Type = string(in.String()) case "roomType": - if in.IsNull() { - in.Skip() - } else { - out.RoomType = string(in.String()) - } + out.RoomType = string(in.String()) case "payload": if in.IsNull() { in.Skip() } else { in.Delim('{') - out.Payload = make(StringMap) + out.Payload = make(map[string]interface{}) for !in.IsDelim('}') { key := string(in.String()) in.WantColon() - var v46 interface{} - if m, ok := v46.(easyjson.Unmarshaler); ok { + var v45 interface{} + if m, ok := v45.(easyjson.Unmarshaler); ok { m.UnmarshalEasyJSON(in) - } else if m, ok := v46.(json.Unmarshaler); ok { + } else if m, ok := v45.(json.Unmarshaler); ok { _ = m.UnmarshalJSON(in.Raw()) } else { - v46 = in.Interface() + v45 = in.Interface() } - (out.Payload)[key] = v46 + (out.Payload)[key] = v45 in.WantComma() } in.Delim('}') } case "sid": - if in.IsNull() { - in.Skip() - } else { - out.Sid = string(in.String()) - } + out.Sid = string(in.String()) default: in.SkipRecursive() } @@ -5921,7 +5375,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(out *jwriter.Writer, in AnswerOfferMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling47(out *jwriter.Writer, in AnswerOfferMessage) { out.RawByte('{') first := true _ = first @@ -5952,21 +5406,21 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(out out.RawString(`null`) } else { out.RawByte('{') - v47First := true - for v47Name, v47Value := range in.Payload { - if v47First { - v47First = false + v46First := true + for v46Name, v46Value := range in.Payload { + if v46First { + v46First = false } else { out.RawByte(',') } - out.String(string(v47Name)) + out.String(string(v46Name)) out.RawByte(':') - if m, ok := v47Value.(easyjson.Marshaler); ok { + if m, ok := v46Value.(easyjson.Marshaler); ok { m.MarshalEasyJSON(out) - } else if m, ok := v47Value.(json.Marshaler); ok { + } else if m, ok := v46Value.(json.Marshaler); ok { out.Raw(m.MarshalJSON()) } else { - out.Raw(json.Marshal(v47Value)) + out.Raw(json.Marshal(v46Value)) } } out.RawByte('}') @@ -5983,27 +5437,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(out // MarshalJSON supports json.Marshaler interface func (v AnswerOfferMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling47(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v AnswerOfferMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling47(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *AnswerOfferMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling47(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AnswerOfferMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api48(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling47(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(in *jlexer.Lexer, out *AddSessionOptions) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling48(in *jlexer.Lexer, out *AddSessionOptions) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -6016,19 +5470,16 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "actorId": - if in.IsNull() { - in.Skip() - } else { - out.ActorId = string(in.String()) - } + out.ActorId = string(in.String()) case "actorType": - if in.IsNull() { - in.Skip() - } else { - out.ActorType = string(in.String()) - } + out.ActorType = string(in.String()) default: in.SkipRecursive() } @@ -6039,7 +5490,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(out *jwriter.Writer, in AddSessionOptions) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling48(out *jwriter.Writer, in AddSessionOptions) { out.RawByte('{') first := true _ = first @@ -6065,27 +5516,27 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(out // MarshalJSON supports json.Marshaler interface func (v AddSessionOptions) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling48(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v AddSessionOptions) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling48(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *AddSessionOptions) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling48(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AddSessionOptions) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api49(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling48(l, v) } -func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(in *jlexer.Lexer, out *AddSessionInternalClientMessage) { +func easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling49(in *jlexer.Lexer, out *AddSessionInternalClientMessage) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -6098,27 +5549,20 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(in for !in.IsDelim('}') { key := in.UnsafeFieldName(false) in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } switch key { case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } + out.UserId = string(in.String()) case "user": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.User).UnmarshalJSON(data)) - } + if data := in.Raw(); in.Ok() { + in.AddError((out.User).UnmarshalJSON(data)) } case "flags": - if in.IsNull() { - in.Skip() - } else { - out.Flags = uint32(in.Uint32()) - } + out.Flags = uint32(in.Uint32()) case "incall": if in.IsNull() { in.Skip() @@ -6127,11 +5571,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(in if out.InCall == nil { out.InCall = new(int) } - if in.IsNull() { - in.Skip() - } else { - *out.InCall = int(in.Int()) - } + *out.InCall = int(in.Int()) } case "options": if in.IsNull() { @@ -6141,24 +5581,12 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(in if out.Options == nil { out.Options = new(AddSessionOptions) } - if in.IsNull() { - in.Skip() - } else { - (*out.Options).UnmarshalEasyJSON(in) - } + (*out.Options).UnmarshalEasyJSON(in) } case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = PublicSessionId(in.String()) - } + out.SessionId = string(in.String()) case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } + out.RoomId = string(in.String()) default: in.SkipRecursive() } @@ -6169,7 +5597,7 @@ func easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(in in.Consumed() } } -func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(out *jwriter.Writer, in AddSessionInternalClientMessage) { +func easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling49(out *jwriter.Writer, in AddSessionInternalClientMessage) { out.RawByte('{') first := true _ = first @@ -6240,23 +5668,23 @@ func easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(out // MarshalJSON supports json.Marshaler interface func (v AddSessionInternalClientMessage) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(&w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling49(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v AddSessionInternalClientMessage) MarshalEasyJSON(w *jwriter.Writer) { - easyjson6128dd2EncodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(w, v) + easyjson29f189fbEncodeGithubComStrukturagNextcloudSpreedSignaling49(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *AddSessionInternalClientMessage) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(&r, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling49(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *AddSessionInternalClientMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson6128dd2DecodeGithubComStrukturagNextcloudSpreedSignalingV2Api50(l, v) + easyjson29f189fbDecodeGithubComStrukturagNextcloudSpreedSignaling49(l, v) } diff --git a/api_signaling_test.go b/api_signaling_test.go new file mode 100644 index 0000000..b33169b --- /dev/null +++ b/api_signaling_test.go @@ -0,0 +1,426 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "encoding/json" + "fmt" + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testCheckValid interface { + CheckValid() error +} + +func wrapMessage(messageType string, msg testCheckValid) *ClientMessage { + wrapped := &ClientMessage{ + Type: messageType, + } + switch messageType { + case "hello": + wrapped.Hello = msg.(*HelloClientMessage) + case "message": + wrapped.Message = msg.(*MessageClientMessage) + case "bye": + wrapped.Bye = msg.(*ByeClientMessage) + case "room": + wrapped.Room = msg.(*RoomClientMessage) + default: + return nil + } + return wrapped +} + +func testMessages(t *testing.T, messageType string, valid_messages []testCheckValid, invalid_messages []testCheckValid) { + t.Helper() + assert := assert.New(t) + for _, msg := range valid_messages { + assert.NoError(msg.CheckValid(), "Message %+v should be valid", msg) + + // If the inner message is valid, it should also be valid in a wrapped + // ClientMessage. + if wrapped := wrapMessage(messageType, msg); assert.NotNil(wrapped, "Unknown message type: %s", messageType) { + assert.NoError(wrapped.CheckValid(), "Message %+v should be valid", wrapped) + } + } + for _, msg := range invalid_messages { + assert.Error(msg.CheckValid(), "Message %+v should not be valid", msg) + + // If the inner message is invalid, it should also be invalid in a + // wrapped ClientMessage. + if wrapped := wrapMessage(messageType, msg); assert.NotNil(wrapped, "Unknown message type: %s", messageType) { + assert.Error(wrapped.CheckValid(), "Message %+v should not be valid", wrapped) + } + } +} + +func TestClientMessage(t *testing.T) { + t.Parallel() + assert := assert.New(t) + // The message needs a type. + msg := ClientMessage{} + assert.Error(msg.CheckValid()) +} + +func TestHelloClientMessage(t *testing.T) { + t.Parallel() + internalAuthParams := []byte("{\"backend\":\"https://domain.invalid\"}") + tokenAuthParams := []byte("{\"token\":\"invalid-token\"}") + valid_messages := []testCheckValid{ + // Hello version 1 + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Params: json.RawMessage("{}"), + Url: "https://domain.invalid", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Type: "client", + Params: json.RawMessage("{}"), + Url: "https://domain.invalid", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Type: "internal", + Params: internalAuthParams, + }, + }, + &HelloClientMessage{ + Version: HelloVersionV1, + ResumeId: "the-resume-id", + }, + // Hello version 2 + &HelloClientMessage{ + Version: HelloVersionV2, + Auth: &HelloClientMessageAuth{ + Params: tokenAuthParams, + Url: "https://domain.invalid", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV2, + Auth: &HelloClientMessageAuth{ + Type: "client", + Params: tokenAuthParams, + Url: "https://domain.invalid", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV2, + ResumeId: "the-resume-id", + }, + } + invalid_messages := []testCheckValid{ + // Hello version 1 + &HelloClientMessage{}, + &HelloClientMessage{Version: "0.0"}, + &HelloClientMessage{Version: HelloVersionV1}, + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Params: json.RawMessage("{}"), + Type: "invalid-type", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Url: "https://domain.invalid", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Params: json.RawMessage("{}"), + }, + }, + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Params: json.RawMessage("{}"), + Url: "invalid-url", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Type: "internal", + Params: json.RawMessage("{}"), + }, + }, + &HelloClientMessage{ + Version: HelloVersionV1, + Auth: &HelloClientMessageAuth{ + Type: "internal", + Params: json.RawMessage("xyz"), // Invalid JSON. + }, + }, + // Hello version 2 + &HelloClientMessage{ + Version: HelloVersionV2, + Auth: &HelloClientMessageAuth{ + Url: "https://domain.invalid", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV2, + Auth: &HelloClientMessageAuth{ + Params: tokenAuthParams, + }, + }, + &HelloClientMessage{ + Version: HelloVersionV2, + Auth: &HelloClientMessageAuth{ + Params: tokenAuthParams, + Url: "invalid-url", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV2, + Auth: &HelloClientMessageAuth{ + Params: internalAuthParams, + Url: "https://domain.invalid", + }, + }, + &HelloClientMessage{ + Version: HelloVersionV2, + Auth: &HelloClientMessageAuth{ + Params: json.RawMessage("xyz"), // Invalid JSON. + Url: "https://domain.invalid", + }, + }, + } + + testMessages(t, "hello", valid_messages, invalid_messages) + + // A "hello" message must be present + msg := ClientMessage{ + Type: "hello", + } + assert := assert.New(t) + assert.Error(msg.CheckValid()) +} + +func TestMessageClientMessage(t *testing.T) { + t.Parallel() + valid_messages := []testCheckValid{ + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "session", + SessionId: "the-session-id", + }, + Data: json.RawMessage("{}"), + }, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "user", + UserId: "the-user-id", + }, + Data: json.RawMessage("{}"), + }, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "room", + }, + Data: json.RawMessage("{}"), + }, + } + invalid_messages := []testCheckValid{ + &MessageClientMessage{}, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "session", + SessionId: "the-session-id", + }, + }, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "session", + }, + Data: json.RawMessage("{}"), + }, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "session", + UserId: "the-user-id", + }, + Data: json.RawMessage("{}"), + }, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "user", + }, + Data: json.RawMessage("{}"), + }, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "user", + UserId: "the-user-id", + }, + }, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "user", + SessionId: "the-user-id", + }, + Data: json.RawMessage("{}"), + }, + &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + Type: "unknown-type", + }, + Data: json.RawMessage("{}"), + }, + } + testMessages(t, "message", valid_messages, invalid_messages) + + // A "message" message must be present + msg := ClientMessage{ + Type: "message", + } + assert := assert.New(t) + assert.Error(msg.CheckValid()) +} + +func TestByeClientMessage(t *testing.T) { + t.Parallel() + // Any "bye" message is valid. + valid_messages := []testCheckValid{ + &ByeClientMessage{}, + } + invalid_messages := []testCheckValid{} + + testMessages(t, "bye", valid_messages, invalid_messages) + + // The "bye" message is optional. + msg := ClientMessage{ + Type: "bye", + } + assert := assert.New(t) + assert.NoError(msg.CheckValid()) +} + +func TestRoomClientMessage(t *testing.T) { + t.Parallel() + // Any "room" message is valid. + valid_messages := []testCheckValid{ + &RoomClientMessage{}, + } + invalid_messages := []testCheckValid{} + + testMessages(t, "room", valid_messages, invalid_messages) + + // But a "room" message must be present + msg := ClientMessage{ + Type: "room", + } + assert := assert.New(t) + assert.Error(msg.CheckValid()) +} + +func TestErrorMessages(t *testing.T) { + t.Parallel() + assert := assert.New(t) + id := "request-id" + msg := ClientMessage{ + Id: id, + } + err1 := msg.NewErrorServerMessage(&Error{}) + assert.Equal(id, err1.Id, "%+v", err1) + assert.Equal("error", err1.Type, "%+v", err1) + assert.NotNil(err1.Error, "%+v", err1) + + err2 := msg.NewWrappedErrorServerMessage(fmt.Errorf("test-error")) + assert.Equal(id, err2.Id, "%+v", err2) + assert.Equal("error", err2.Type, "%+v", err2) + if assert.NotNil(err2.Error, "%+v", err2) { + assert.Equal("internal_error", err2.Error.Code, "%+v", err2) + assert.Equal("test-error", err2.Error.Message, "%+v", err2) + } + // Test "error" interface + assert.Equal("test-error", err2.Error.Error(), "%+v", err2) +} + +func TestIsChatRefresh(t *testing.T) { + t.Parallel() + var msg ServerMessage + data_true := []byte("{\"type\":\"chat\",\"chat\":{\"refresh\":true}}") + msg = ServerMessage{ + Type: "message", + Message: &MessageServerMessage{ + Data: data_true, + }, + } + assert.True(t, msg.IsChatRefresh()) + + data_false := []byte("{\"type\":\"chat\",\"chat\":{\"refresh\":false}}") + msg = ServerMessage{ + Type: "message", + Message: &MessageServerMessage{ + Data: data_false, + }, + } + assert.False(t, msg.IsChatRefresh()) +} + +func assertEqualStrings(t *testing.T, expected, result []string) { + t.Helper() + + if expected == nil { + expected = make([]string, 0) + } else { + sort.Strings(expected) + } + if result == nil { + result = make([]string, 0) + } else { + sort.Strings(result) + } + + assert.Equal(t, expected, result) +} + +func Test_Welcome_AddRemoveFeature(t *testing.T) { + t.Parallel() + assert := assert.New(t) + var msg WelcomeServerMessage + assertEqualStrings(t, []string{}, msg.Features) + + msg.AddFeature("one", "two", "one") + assertEqualStrings(t, []string{"one", "two"}, msg.Features) + assert.True(sort.StringsAreSorted(msg.Features), "features should be sorted, got %+v", msg.Features) + + msg.AddFeature("three") + assertEqualStrings(t, []string{"one", "two", "three"}, msg.Features) + assert.True(sort.StringsAreSorted(msg.Features), "features should be sorted, got %+v", msg.Features) + + msg.RemoveFeature("three", "one") + assertEqualStrings(t, []string{"two"}, msg.Features) +} diff --git a/async/events/async_events.go b/async/events/async_events.go deleted file mode 100644 index aec0baa..0000000 --- a/async/events/async_events.go +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2022 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package events - -import ( - "context" - "errors" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -var ( - ErrAlreadyRegistered = errors.New("already registered") // +checklocksignore: Global readonly variable. -) - -const ( - DefaultAsyncChannelSize = 64 -) - -type AsyncChannel chan *nats.Msg - -type AsyncEventListener interface { - AsyncChannel() AsyncChannel -} - -type AsyncEvents interface { - Close(ctx context.Context) error - - RegisterBackendRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error - UnregisterBackendRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error - - RegisterRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error - UnregisterRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error - - RegisterUserListener(userId string, backend *talk.Backend, listener AsyncEventListener) error - UnregisterUserListener(userId string, backend *talk.Backend, listener AsyncEventListener) error - - RegisterSessionListener(sessionId api.PublicSessionId, backend *talk.Backend, listener AsyncEventListener) error - UnregisterSessionListener(sessionId api.PublicSessionId, backend *talk.Backend, listener AsyncEventListener) error - - PublishBackendRoomMessage(roomId string, backend *talk.Backend, message *AsyncMessage) error - PublishRoomMessage(roomId string, backend *talk.Backend, message *AsyncMessage) error - PublishUserMessage(userId string, backend *talk.Backend, message *AsyncMessage) error - PublishSessionMessage(sessionId api.PublicSessionId, backend *talk.Backend, message *AsyncMessage) error -} - -func NewAsyncEvents(ctx context.Context, url string) (AsyncEvents, error) { - client, err := nats.NewClient(ctx, url) - if err != nil { - return nil, err - } - - return NewAsyncEventsNats(log.LoggerFromContext(ctx), client) -} diff --git a/async/events/async_events_nats.go b/async/events/async_events_nats.go deleted file mode 100644 index 4cc776d..0000000 --- a/async/events/async_events_nats.go +++ /dev/null @@ -1,292 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2022 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package events - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -func GetSubjectForBackendRoomId(roomId string, backend *talk.Backend) string { - if backend == nil || backend.IsCompat() { - return nats.GetEncodedSubject("backend.room", roomId) - } - - return nats.GetEncodedSubject("backend.room", roomId+"|"+backend.Id()) -} - -func GetSubjectForRoomId(roomId string, backend *talk.Backend) string { - if backend == nil || backend.IsCompat() { - return nats.GetEncodedSubject("room", roomId) - } - - return nats.GetEncodedSubject("room", roomId+"|"+backend.Id()) -} - -func GetSubjectForUserId(userId string, backend *talk.Backend) string { - if backend == nil || backend.IsCompat() { - return nats.GetEncodedSubject("user", userId) - } - - return nats.GetEncodedSubject("user", userId+"|"+backend.Id()) -} - -func GetSubjectForSessionId(sessionId api.PublicSessionId, backend *talk.Backend) string { - return string("session." + sessionId) -} - -type asyncEventsNatsSubscriptions map[string]map[AsyncEventListener]nats.Subscription - -type asyncEventsNats struct { - mu sync.Mutex - client nats.Client - logger log.Logger // +checklocksignore - - // +checklocks:mu - backendRoomSubscriptions asyncEventsNatsSubscriptions - // +checklocks:mu - roomSubscriptions asyncEventsNatsSubscriptions - // +checklocks:mu - userSubscriptions asyncEventsNatsSubscriptions - // +checklocks:mu - sessionSubscriptions asyncEventsNatsSubscriptions -} - -func NewAsyncEventsNats(logger log.Logger, client nats.Client) (AsyncEvents, error) { - events := &asyncEventsNats{ - client: client, - logger: logger, - - backendRoomSubscriptions: make(asyncEventsNatsSubscriptions), - roomSubscriptions: make(asyncEventsNatsSubscriptions), - userSubscriptions: make(asyncEventsNatsSubscriptions), - sessionSubscriptions: make(asyncEventsNatsSubscriptions), - } - return events, nil -} - -func (e *asyncEventsNats) GetNatsClient() nats.Client { - return e.client -} - -func (e *asyncEventsNats) GetServerInfoNats() *talk.BackendServerInfoNats { - // TODO: This should call a method on "e.client" directly instead of having a type switch. - var result *talk.BackendServerInfoNats - switch n := e.client.(type) { - case *nats.NativeClient: - result = &talk.BackendServerInfoNats{ - Urls: n.URLs(), - } - if n.IsConnected() { - result.Connected = true - result.ServerUrl = n.ConnectedUrl() - result.ServerID = n.ConnectedServerId() - result.ServerVersion = n.ConnectedServerVersion() - result.ClusterName = n.ConnectedClusterName() - } - case *nats.LoopbackClient: - result = &talk.BackendServerInfoNats{ - Urls: []string{nats.LoopbackUrl}, - Connected: true, - ServerUrl: nats.LoopbackUrl, - } - } - - return result -} - -func closeSubscriptions(logger log.Logger, wg *sync.WaitGroup, subscriptions asyncEventsNatsSubscriptions) { - defer wg.Done() - - for subject, subs := range subscriptions { - for _, sub := range subs { - if err := sub.Unsubscribe(); err != nil && !errors.Is(err, nats.ErrConnectionClosed) { - logger.Printf("Error unsubscribing %s: %s", subject, err) - } - } - } -} - -func (e *asyncEventsNats) Close(ctx context.Context) error { - e.mu.Lock() - defer e.mu.Unlock() - var wg sync.WaitGroup - wg.Add(1) - go closeSubscriptions(e.logger, &wg, e.backendRoomSubscriptions) - wg.Add(1) - go closeSubscriptions(e.logger, &wg, e.roomSubscriptions) - wg.Add(1) - go closeSubscriptions(e.logger, &wg, e.userSubscriptions) - wg.Add(1) - go closeSubscriptions(e.logger, &wg, e.sessionSubscriptions) - // Can't use clear(...) here as the maps are processed asynchronously by the - // goroutines above. - e.backendRoomSubscriptions = make(asyncEventsNatsSubscriptions) - e.roomSubscriptions = make(asyncEventsNatsSubscriptions) - e.userSubscriptions = make(asyncEventsNatsSubscriptions) - e.sessionSubscriptions = make(asyncEventsNatsSubscriptions) - wg.Wait() - return e.client.Close(ctx) -} - -// +checklocks:e.mu -func (e *asyncEventsNats) registerListener(key string, subscriptions asyncEventsNatsSubscriptions, listener AsyncEventListener) error { - subs, found := subscriptions[key] - if !found { - subs = make(map[AsyncEventListener]nats.Subscription) - subscriptions[key] = subs - } else if _, found := subs[listener]; found { - return ErrAlreadyRegistered - } - - sub, err := e.client.Subscribe(key, listener.AsyncChannel()) - if err != nil { - return err - } - - subs[listener] = sub - return nil -} - -// +checklocks:e.mu -func (e *asyncEventsNats) unregisterListener(key string, subscriptions asyncEventsNatsSubscriptions, listener AsyncEventListener) error { - subs, found := subscriptions[key] - if !found { - return nil - } - - sub, found := subs[listener] - if !found { - return nil - } - - delete(subs, listener) - if len(subs) == 0 { - delete(subscriptions, key) - } - - return sub.Unsubscribe() -} - -func (e *asyncEventsNats) RegisterBackendRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error { - key := GetSubjectForBackendRoomId(roomId, backend) - - e.mu.Lock() - defer e.mu.Unlock() - - return e.registerListener(key, e.backendRoomSubscriptions, listener) -} - -func (e *asyncEventsNats) UnregisterBackendRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error { - key := GetSubjectForBackendRoomId(roomId, backend) - - e.mu.Lock() - defer e.mu.Unlock() - - return e.unregisterListener(key, e.backendRoomSubscriptions, listener) -} - -func (e *asyncEventsNats) RegisterRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error { - key := GetSubjectForRoomId(roomId, backend) - - e.mu.Lock() - defer e.mu.Unlock() - - return e.registerListener(key, e.roomSubscriptions, listener) -} - -func (e *asyncEventsNats) UnregisterRoomListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error { - key := GetSubjectForRoomId(roomId, backend) - - e.mu.Lock() - defer e.mu.Unlock() - - return e.unregisterListener(key, e.roomSubscriptions, listener) -} - -func (e *asyncEventsNats) RegisterUserListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error { - key := GetSubjectForUserId(roomId, backend) - - e.mu.Lock() - defer e.mu.Unlock() - - return e.registerListener(key, e.userSubscriptions, listener) -} - -func (e *asyncEventsNats) UnregisterUserListener(roomId string, backend *talk.Backend, listener AsyncEventListener) error { - key := GetSubjectForUserId(roomId, backend) - - e.mu.Lock() - defer e.mu.Unlock() - - return e.unregisterListener(key, e.userSubscriptions, listener) -} - -func (e *asyncEventsNats) RegisterSessionListener(sessionId api.PublicSessionId, backend *talk.Backend, listener AsyncEventListener) error { - key := GetSubjectForSessionId(sessionId, backend) - - e.mu.Lock() - defer e.mu.Unlock() - - return e.registerListener(key, e.sessionSubscriptions, listener) -} - -func (e *asyncEventsNats) UnregisterSessionListener(sessionId api.PublicSessionId, backend *talk.Backend, listener AsyncEventListener) error { - key := GetSubjectForSessionId(sessionId, backend) - - e.mu.Lock() - defer e.mu.Unlock() - - return e.unregisterListener(key, e.sessionSubscriptions, listener) -} - -func (e *asyncEventsNats) publish(subject string, message *AsyncMessage) error { - message.SendTime = time.Now().Truncate(time.Microsecond) - return e.client.Publish(subject, message) -} - -func (e *asyncEventsNats) PublishBackendRoomMessage(roomId string, backend *talk.Backend, message *AsyncMessage) error { - subject := GetSubjectForBackendRoomId(roomId, backend) - return e.publish(subject, message) -} - -func (e *asyncEventsNats) PublishRoomMessage(roomId string, backend *talk.Backend, message *AsyncMessage) error { - subject := GetSubjectForRoomId(roomId, backend) - return e.publish(subject, message) -} - -func (e *asyncEventsNats) PublishUserMessage(userId string, backend *talk.Backend, message *AsyncMessage) error { - subject := GetSubjectForUserId(userId, backend) - return e.publish(subject, message) -} - -func (e *asyncEventsNats) PublishSessionMessage(sessionId api.PublicSessionId, backend *talk.Backend, message *AsyncMessage) error { - subject := GetSubjectForSessionId(sessionId, backend) - return e.publish(subject, message) -} diff --git a/async/events/async_events_nats_test.go b/async/events/async_events_nats_test.go deleted file mode 100644 index 45f28f6..0000000 --- a/async/events/async_events_nats_test.go +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package events - -import ( - "testing" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -func Benchmark_GetSubjectForSessionId(b *testing.B) { - backend := talk.NewCompatBackend(nil) - sid := api.PublicSessionId(internal.RandomString(256)) - for b.Loop() { - GetSubjectForSessionId(sid, backend) - } -} diff --git a/async/events/async_events_test.go b/async/events/async_events_test.go deleted file mode 100644 index deb5f41..0000000 --- a/async/events/async_events_test.go +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package events - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - natstest "github.com/strukturag/nextcloud-spreed-signaling/v2/nats/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -type TestBackendRoomListener struct { - events AsyncChannel -} - -func (l *TestBackendRoomListener) AsyncChannel() AsyncChannel { - return l.events -} - -func testAsyncEvents(t *testing.T, events AsyncEvents) { - require := require.New(t) - assert := assert.New(t) - t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - assert.NoError(events.Close(ctx)) - }) - - listener := &TestBackendRoomListener{ - events: make(AsyncChannel, 1), - } - - roomId := "1234" - backend := talk.NewCompatBackend(nil) - require.NoError(events.RegisterBackendRoomListener(roomId, backend, listener)) - defer func() { - assert.NoError(events.UnregisterBackendRoomListener(roomId, backend, listener)) - }() - - msg := &AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "test", - }, - } - if assert.NoError(events.PublishBackendRoomMessage(roomId, backend, msg)) { - received := <-listener.events - var receivedMsg AsyncMessage - if assert.NoError(nats.Decode(received, &receivedMsg)) { - assert.True(msg.SendTime.Equal(receivedMsg.SendTime), "send times don't match, expected %s, got %s", msg.SendTime, receivedMsg.SendTime) - receivedMsg.SendTime = msg.SendTime - assert.Equal(msg, &receivedMsg) - } - } - - require.NoError(events.RegisterRoomListener(roomId, backend, listener)) - defer func() { - assert.NoError(events.UnregisterRoomListener(roomId, backend, listener)) - }() - - roomMessage := &AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "other-test", - }, - } - if assert.NoError(events.PublishRoomMessage(roomId, backend, roomMessage)) { - received := <-listener.events - var receivedMsg AsyncMessage - if assert.NoError(nats.Decode(received, &receivedMsg)) { - assert.True(roomMessage.SendTime.Equal(receivedMsg.SendTime), "send times don't match, expected %s, got %s", roomMessage.SendTime, receivedMsg.SendTime) - receivedMsg.SendTime = roomMessage.SendTime - assert.Equal(roomMessage, &receivedMsg) - } - } - - userId := "the-user" - require.NoError(events.RegisterUserListener(userId, backend, listener)) - defer func() { - assert.NoError(events.UnregisterUserListener(userId, backend, listener)) - }() - - userMessage := &AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "user-test", - }, - } - if assert.NoError(events.PublishUserMessage(userId, backend, userMessage)) { - received := <-listener.events - var receivedMsg AsyncMessage - if assert.NoError(nats.Decode(received, &receivedMsg)) { - assert.True(userMessage.SendTime.Equal(receivedMsg.SendTime), "send times don't match, expected %s, got %s", userMessage.SendTime, receivedMsg.SendTime) - receivedMsg.SendTime = userMessage.SendTime - assert.Equal(userMessage, &receivedMsg) - } - } - - sessionId := api.PublicSessionId("the-session") - require.NoError(events.RegisterSessionListener(sessionId, backend, listener)) - defer func() { - assert.NoError(events.UnregisterSessionListener(sessionId, backend, listener)) - }() - - sessionMessage := &AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "session-test", - }, - } - if assert.NoError(events.PublishSessionMessage(sessionId, backend, sessionMessage)) { - received := <-listener.events - var receivedMsg AsyncMessage - if assert.NoError(nats.Decode(received, &receivedMsg)) { - assert.True(sessionMessage.SendTime.Equal(receivedMsg.SendTime), "send times don't match, expected %s, got %s", sessionMessage.SendTime, receivedMsg.SendTime) - receivedMsg.SendTime = sessionMessage.SendTime - assert.Equal(sessionMessage, &receivedMsg) - } - } -} - -func TestAsyncEvents_Loopback(t *testing.T) { - t.Parallel() - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - events, err := NewAsyncEvents(ctx, nats.LoopbackUrl) - require.NoError(t, err) - testAsyncEvents(t, events) -} - -func TestAsyncEvents_NATS(t *testing.T) { - t.Parallel() - - server, _ := natstest.StartLocalServer(t) - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - events, err := NewAsyncEvents(ctx, server.ClientURL()) - require.NoError(t, err) - testAsyncEvents(t, events) -} diff --git a/async/events/test/events.go b/async/events/test/events.go deleted file mode 100644 index e5d52d1..0000000 --- a/async/events/test/events.go +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - natstest "github.com/strukturag/nextcloud-spreed-signaling/v2/nats/test" -) - -var ( - testTimeout = 10 * time.Second - - EventBackendsForTest = []string{ - "loopback", - "nats", - } -) - -func GetAsyncEventsForTest(t *testing.T) events.AsyncEvents { - var events events.AsyncEvents - if strings.HasSuffix(t.Name(), "/nats") { - events = getRealAsyncEventsForTest(t) - } else { - events = getLoopbackAsyncEventsForTest(t) - } - t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - assert.NoError(t, events.Close(ctx)) - }) - return events -} - -func getRealAsyncEventsForTest(t *testing.T) events.AsyncEvents { - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - server, _ := natstest.StartLocalServer(t) - events, err := events.NewAsyncEvents(ctx, server.ClientURL()) - require.NoError(t, err) - return events -} - -type natsEvents interface { - GetNatsClient() nats.Client -} - -func getLoopbackAsyncEventsForTest(t *testing.T) events.AsyncEvents { - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - events, err := events.NewAsyncEvents(ctx, nats.LoopbackUrl) - require.NoError(t, err) - - t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - e, ok := (events.(natsEvents)) - if !ok { - // Only can wait for NATS events. - return - } - - natstest.WaitForSubscriptionsEmpty(ctx, t, e.GetNatsClient()) - }) - return events -} - -func WaitForAsyncEventsFlushed(ctx context.Context, t *testing.T, events events.AsyncEvents) { - t.Helper() - - e, ok := (events.(natsEvents)) - if !ok { - // Only can wait for NATS events. - return - } - - client, ok := e.GetNatsClient().(*nats.NativeClient) - if !ok { - // The loopback NATS clients is executing all events synchronously. - return - } - - assert.NoError(t, client.FlushWithContext(ctx)) -} diff --git a/async/events/test/events_test.go b/async/events/test/events_test.go deleted file mode 100644 index b353385..0000000 --- a/async/events/test/events_test.go +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -type testListener struct { - ch events.AsyncChannel -} - -func (l *testListener) AsyncChannel() events.AsyncChannel { - return l.ch -} - -func TestAsyncEventsTest(t *testing.T) { - t.Parallel() - - for _, backend := range EventBackendsForTest { - t.Run(backend, func(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - eventsHandler := GetAsyncEventsForTest(t) - - listener := &testListener{ - ch: make(events.AsyncChannel, 1), - } - sessionId := api.PublicSessionId("foo") - backend := talk.NewCompatBackend(nil) - require.NoError(eventsHandler.RegisterSessionListener(sessionId, backend, listener)) - defer func() { - assert.NoError(eventsHandler.UnregisterSessionListener(sessionId, backend, listener)) - }() - - msg := events.AsyncMessage{ - Type: "message", - Message: &api.ServerMessage{ - Type: "error", - Error: api.NewError("test_error", "This is a test error."), - }, - } - if err := eventsHandler.PublishSessionMessage(sessionId, backend, &msg); assert.NoError(err) { - select { - case natsMsg := <-listener.ch: - var received events.AsyncMessage - if err := nats.Decode(natsMsg, &received); assert.NoError(err) { - assert.True(msg.SendTime.Equal(received.SendTime), "send times don't match, expected %s, got %s", msg.SendTime, received.SendTime) - received.SendTime = msg.SendTime - assert.Equal(msg, received) - } - case <-ctx.Done(): - require.NoError(ctx.Err()) - } - } - - WaitForAsyncEventsFlushed(ctx, t, eventsHandler) - }) - } -} diff --git a/async/throttle_test.go b/async/throttle_test.go deleted file mode 100644 index 97d1313..0000000 --- a/async/throttle_test.go +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2024 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package async - -import ( - "testing" - "testing/synctest" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" -) - -func newMemoryThrottlerForTest(t *testing.T) Throttler { - t.Helper() - result, err := NewMemoryThrottler() - require.NoError(t, err) - - t.Cleanup(func() { - result.Close() - }) - - return result -} - -func expectDelay(t *testing.T, f func(), delay time.Duration) { - t.Helper() - a := time.Now() - f() - b := time.Now() - assert.Equal(t, delay, b.Sub(a)) -} - -func TestThrottler(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - th := newMemoryThrottlerForTest(t) - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle1(ctx) - }, 100*time.Millisecond) - - throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle2(ctx) - }, 200*time.Millisecond) - - throttle3, err := th.CheckBruteforce(ctx, "192.168.0.2", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle3(ctx) - }, 100*time.Millisecond) - - throttle4, err := th.CheckBruteforce(ctx, "192.168.0.1", "action2") - assert.NoError(err) - expectDelay(t, func() { - throttle4(ctx) - }, 100*time.Millisecond) - }) -} - -func TestThrottlerIPv6(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - th := newMemoryThrottlerForTest(t) - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - // Make sure full /64 subnets are throttled for IPv6. - throttle1, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle1(ctx) - }, 100*time.Millisecond) - - throttle2, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::2", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle2(ctx) - }, 200*time.Millisecond) - - // A diffent /64 subnet is not throttled yet. - throttle3, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0013::1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle3(ctx) - }, 100*time.Millisecond) - - // A different action is not throttled. - throttle4, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::1", "action2") - assert.NoError(err) - expectDelay(t, func() { - throttle4(ctx) - }, 100*time.Millisecond) - }) -} - -func TestThrottler_Bruteforce(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - th := newMemoryThrottlerForTest(t) - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - delay := 100 * time.Millisecond - for range maxBruteforceAttempts { - throttle, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle(ctx) - }, delay) - delay *= 2 - if delay > maxThrottleDelay { - delay = maxThrottleDelay - } - } - - _, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.ErrorIs(err, ErrBruteforceDetected) - }) -} - -func TestThrottler_Cleanup(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - throttler := newMemoryThrottlerForTest(t) - th, ok := throttler.(*memoryThrottler) - require.True(t, ok, "required memoryThrottler, got %T", throttler) - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle1(ctx) - }, 100*time.Millisecond) - - throttle2, err := th.CheckBruteforce(ctx, "192.168.0.2", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle2(ctx) - }, 100*time.Millisecond) - - time.Sleep(time.Hour) - - throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action2") - assert.NoError(err) - expectDelay(t, func() { - throttle3(ctx) - }, 100*time.Millisecond) - - throttle4, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle4(ctx) - }, 200*time.Millisecond) - - cleanupNow := time.Now().Add(-time.Hour).Add(maxBruteforceAge).Add(time.Second) - th.cleanup(cleanupNow) - - assert.Len(th.getEntries("192.168.0.1", "action1"), 1) - assert.Len(th.getEntries("192.168.0.1", "action2"), 1) - - th.mu.RLock() - if entries, found := th.clients["192.168.0.2"]; found { - assert.Fail("should have removed client \"192.168.0.2\"", "got %+v", entries) - } - th.mu.RUnlock() - - throttle5, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle5(ctx) - }, 200*time.Millisecond) - }) -} - -func TestThrottler_ExpirePartial(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - th := newMemoryThrottlerForTest(t) - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle1(ctx) - }, 100*time.Millisecond) - - time.Sleep(time.Minute) - - throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle2(ctx) - }, 200*time.Millisecond) - - time.Sleep(maxBruteforceAge - time.Minute + time.Second) - - throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle3(ctx) - }, 200*time.Millisecond) - }) -} - -func TestThrottler_ExpireAll(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - th := newMemoryThrottlerForTest(t) - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle1(ctx) - }, 100*time.Millisecond) - - time.Sleep(time.Millisecond) - - throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle2(ctx) - }, 200*time.Millisecond) - - time.Sleep(maxBruteforceAge + time.Second) - - throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - assert.NoError(err) - expectDelay(t, func() { - throttle3(ctx) - }, 100*time.Millisecond) - }) -} - -func TestThrottler_Negative(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - th := newMemoryThrottlerForTest(t) - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - delay := 100 * time.Millisecond - for range maxBruteforceAttempts * 10 { - throttle, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") - if err != nil { - assert.ErrorIs(err, ErrBruteforceDetected) - } - expectDelay(t, func() { - throttle(ctx) - }, delay) - delay *= 2 - if delay > maxThrottleDelay { - delay = maxThrottleDelay - } - } - }) -} diff --git a/async_events.go b/async_events.go new file mode 100644 index 0000000..4fb34d8 --- /dev/null +++ b/async_events.go @@ -0,0 +1,210 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import "sync" + +type AsyncBackendRoomEventListener interface { + ProcessBackendRoomRequest(message *AsyncMessage) +} + +type AsyncRoomEventListener interface { + ProcessAsyncRoomMessage(message *AsyncMessage) +} + +type AsyncUserEventListener interface { + ProcessAsyncUserMessage(message *AsyncMessage) +} + +type AsyncSessionEventListener interface { + ProcessAsyncSessionMessage(message *AsyncMessage) +} + +type AsyncEvents interface { + Close() + + RegisterBackendRoomListener(roomId string, backend *Backend, listener AsyncBackendRoomEventListener) error + UnregisterBackendRoomListener(roomId string, backend *Backend, listener AsyncBackendRoomEventListener) + + RegisterRoomListener(roomId string, backend *Backend, listener AsyncRoomEventListener) error + UnregisterRoomListener(roomId string, backend *Backend, listener AsyncRoomEventListener) + + RegisterUserListener(userId string, backend *Backend, listener AsyncUserEventListener) error + UnregisterUserListener(userId string, backend *Backend, listener AsyncUserEventListener) + + RegisterSessionListener(sessionId string, backend *Backend, listener AsyncSessionEventListener) error + UnregisterSessionListener(sessionId string, backend *Backend, listener AsyncSessionEventListener) + + PublishBackendRoomMessage(roomId string, backend *Backend, message *AsyncMessage) error + PublishRoomMessage(roomId string, backend *Backend, message *AsyncMessage) error + PublishUserMessage(userId string, backend *Backend, message *AsyncMessage) error + PublishSessionMessage(sessionId string, backend *Backend, message *AsyncMessage) error +} + +func NewAsyncEvents(url string) (AsyncEvents, error) { + client, err := NewNatsClient(url) + if err != nil { + return nil, err + } + + return NewAsyncEventsNats(client) +} + +type asyncBackendRoomSubscriber struct { + mu sync.Mutex + + listeners map[AsyncBackendRoomEventListener]bool +} + +func (s *asyncBackendRoomSubscriber) processBackendRoomRequest(message *AsyncMessage) { + s.mu.Lock() + defer s.mu.Unlock() + + for listener := range s.listeners { + s.mu.Unlock() + listener.ProcessBackendRoomRequest(message) + s.mu.Lock() + } +} + +func (s *asyncBackendRoomSubscriber) addListener(listener AsyncBackendRoomEventListener) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.listeners == nil { + s.listeners = make(map[AsyncBackendRoomEventListener]bool) + } + s.listeners[listener] = true +} + +func (s *asyncBackendRoomSubscriber) removeListener(listener AsyncBackendRoomEventListener) bool { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.listeners, listener) + return len(s.listeners) > 0 +} + +type asyncRoomSubscriber struct { + mu sync.Mutex + + listeners map[AsyncRoomEventListener]bool +} + +func (s *asyncRoomSubscriber) processAsyncRoomMessage(message *AsyncMessage) { + s.mu.Lock() + defer s.mu.Unlock() + + for listener := range s.listeners { + s.mu.Unlock() + listener.ProcessAsyncRoomMessage(message) + s.mu.Lock() + } +} + +func (s *asyncRoomSubscriber) addListener(listener AsyncRoomEventListener) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.listeners == nil { + s.listeners = make(map[AsyncRoomEventListener]bool) + } + s.listeners[listener] = true +} + +func (s *asyncRoomSubscriber) removeListener(listener AsyncRoomEventListener) bool { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.listeners, listener) + return len(s.listeners) > 0 +} + +type asyncUserSubscriber struct { + mu sync.Mutex + + listeners map[AsyncUserEventListener]bool +} + +func (s *asyncUserSubscriber) processAsyncUserMessage(message *AsyncMessage) { + s.mu.Lock() + defer s.mu.Unlock() + + for listener := range s.listeners { + s.mu.Unlock() + listener.ProcessAsyncUserMessage(message) + s.mu.Lock() + } +} + +func (s *asyncUserSubscriber) addListener(listener AsyncUserEventListener) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.listeners == nil { + s.listeners = make(map[AsyncUserEventListener]bool) + } + s.listeners[listener] = true +} + +func (s *asyncUserSubscriber) removeListener(listener AsyncUserEventListener) bool { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.listeners, listener) + return len(s.listeners) > 0 +} + +type asyncSessionSubscriber struct { + mu sync.Mutex + + listeners map[AsyncSessionEventListener]bool +} + +func (s *asyncSessionSubscriber) processAsyncSessionMessage(message *AsyncMessage) { + s.mu.Lock() + defer s.mu.Unlock() + + for listener := range s.listeners { + s.mu.Unlock() + listener.ProcessAsyncSessionMessage(message) + s.mu.Lock() + } +} + +func (s *asyncSessionSubscriber) addListener(listener AsyncSessionEventListener) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.listeners == nil { + s.listeners = make(map[AsyncSessionEventListener]bool) + } + s.listeners[listener] = true +} + +func (s *asyncSessionSubscriber) removeListener(listener AsyncSessionEventListener) bool { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.listeners, listener) + return len(s.listeners) > 0 +} diff --git a/async_events_nats.go b/async_events_nats.go new file mode 100644 index 0000000..0db3502 --- /dev/null +++ b/async_events_nats.go @@ -0,0 +1,452 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "log" + "sync" + "time" + + "github.com/nats-io/nats.go" +) + +func GetSubjectForBackendRoomId(roomId string, backend *Backend) string { + if backend == nil || backend.IsCompat() { + return GetEncodedSubject("backend.room", roomId) + } + + return GetEncodedSubject("backend.room", roomId+"|"+backend.Id()) +} + +func GetSubjectForRoomId(roomId string, backend *Backend) string { + if backend == nil || backend.IsCompat() { + return GetEncodedSubject("room", roomId) + } + + return GetEncodedSubject("room", roomId+"|"+backend.Id()) +} + +func GetSubjectForUserId(userId string, backend *Backend) string { + if backend == nil || backend.IsCompat() { + return GetEncodedSubject("user", userId) + } + + return GetEncodedSubject("user", userId+"|"+backend.Id()) +} + +func GetSubjectForSessionId(sessionId string, backend *Backend) string { + return "session." + sessionId +} + +type asyncSubscriberNats struct { + key string + client NatsClient + + receiver chan *nats.Msg + closeChan chan struct{} + subscription NatsSubscription + + processMessage func(*nats.Msg) +} + +func newAsyncSubscriberNats(key string, client NatsClient) (*asyncSubscriberNats, error) { + receiver := make(chan *nats.Msg, 64) + sub, err := client.Subscribe(key, receiver) + if err != nil { + return nil, err + } + + result := &asyncSubscriberNats{ + key: key, + client: client, + + receiver: receiver, + closeChan: make(chan struct{}), + subscription: sub, + } + return result, nil +} + +func (s *asyncSubscriberNats) run() { + defer func() { + if err := s.subscription.Unsubscribe(); err != nil { + log.Printf("Error unsubscribing %s: %s", s.key, err) + } + }() + + for { + select { + case msg := <-s.receiver: + s.processMessage(msg) + for count := len(s.receiver); count > 0; count-- { + s.processMessage(<-s.receiver) + } + case <-s.closeChan: + return + } + } +} + +func (s *asyncSubscriberNats) close() { + close(s.closeChan) +} + +type asyncBackendRoomSubscriberNats struct { + *asyncSubscriberNats + asyncBackendRoomSubscriber +} + +func newAsyncBackendRoomSubscriberNats(key string, client NatsClient) (*asyncBackendRoomSubscriberNats, error) { + sub, err := newAsyncSubscriberNats(key, client) + if err != nil { + return nil, err + } + + result := &asyncBackendRoomSubscriberNats{ + asyncSubscriberNats: sub, + } + result.processMessage = result.doProcessMessage + go result.run() + return result, nil +} + +func (s *asyncBackendRoomSubscriberNats) doProcessMessage(msg *nats.Msg) { + var message AsyncMessage + if err := s.client.Decode(msg, &message); err != nil { + log.Printf("Could not decode NATS message %+v, %s", msg, err) + return + } + + s.processBackendRoomRequest(&message) +} + +type asyncRoomSubscriberNats struct { + asyncRoomSubscriber + *asyncSubscriberNats +} + +func newAsyncRoomSubscriberNats(key string, client NatsClient) (*asyncRoomSubscriberNats, error) { + sub, err := newAsyncSubscriberNats(key, client) + if err != nil { + return nil, err + } + + result := &asyncRoomSubscriberNats{ + asyncSubscriberNats: sub, + } + result.processMessage = result.doProcessMessage + go result.run() + return result, nil +} + +func (s *asyncRoomSubscriberNats) doProcessMessage(msg *nats.Msg) { + var message AsyncMessage + if err := s.client.Decode(msg, &message); err != nil { + log.Printf("Could not decode nats message %+v, %s", msg, err) + return + } + + s.processAsyncRoomMessage(&message) +} + +type asyncUserSubscriberNats struct { + *asyncSubscriberNats + asyncUserSubscriber +} + +func newAsyncUserSubscriberNats(key string, client NatsClient) (*asyncUserSubscriberNats, error) { + sub, err := newAsyncSubscriberNats(key, client) + if err != nil { + return nil, err + } + + result := &asyncUserSubscriberNats{ + asyncSubscriberNats: sub, + } + result.processMessage = result.doProcessMessage + go result.run() + return result, nil +} + +func (s *asyncUserSubscriberNats) doProcessMessage(msg *nats.Msg) { + var message AsyncMessage + if err := s.client.Decode(msg, &message); err != nil { + log.Printf("Could not decode nats message %+v, %s", msg, err) + return + } + + s.processAsyncUserMessage(&message) +} + +type asyncSessionSubscriberNats struct { + *asyncSubscriberNats + asyncSessionSubscriber +} + +func newAsyncSessionSubscriberNats(key string, client NatsClient) (*asyncSessionSubscriberNats, error) { + sub, err := newAsyncSubscriberNats(key, client) + if err != nil { + return nil, err + } + + result := &asyncSessionSubscriberNats{ + asyncSubscriberNats: sub, + } + result.processMessage = result.doProcessMessage + go result.run() + return result, nil +} + +func (s *asyncSessionSubscriberNats) doProcessMessage(msg *nats.Msg) { + var message AsyncMessage + if err := s.client.Decode(msg, &message); err != nil { + log.Printf("Could not decode nats message %+v, %s", msg, err) + return + } + + s.processAsyncSessionMessage(&message) +} + +type asyncEventsNats struct { + mu sync.Mutex + client NatsClient + + backendRoomSubscriptions map[string]*asyncBackendRoomSubscriberNats + roomSubscriptions map[string]*asyncRoomSubscriberNats + userSubscriptions map[string]*asyncUserSubscriberNats + sessionSubscriptions map[string]*asyncSessionSubscriberNats +} + +func NewAsyncEventsNats(client NatsClient) (AsyncEvents, error) { + events := &asyncEventsNats{ + client: client, + + backendRoomSubscriptions: make(map[string]*asyncBackendRoomSubscriberNats), + roomSubscriptions: make(map[string]*asyncRoomSubscriberNats), + userSubscriptions: make(map[string]*asyncUserSubscriberNats), + sessionSubscriptions: make(map[string]*asyncSessionSubscriberNats), + } + return events, nil +} + +func (e *asyncEventsNats) Close() { + e.mu.Lock() + defer e.mu.Unlock() + var wg sync.WaitGroup + wg.Add(1) + go func(subscriptions map[string]*asyncBackendRoomSubscriberNats) { + defer wg.Done() + for _, sub := range subscriptions { + sub.close() + } + }(e.backendRoomSubscriptions) + wg.Add(1) + go func(subscriptions map[string]*asyncRoomSubscriberNats) { + defer wg.Done() + for _, sub := range subscriptions { + sub.close() + } + }(e.roomSubscriptions) + wg.Add(1) + go func(subscriptions map[string]*asyncUserSubscriberNats) { + defer wg.Done() + for _, sub := range subscriptions { + sub.close() + } + }(e.userSubscriptions) + wg.Add(1) + go func(subscriptions map[string]*asyncSessionSubscriberNats) { + defer wg.Done() + for _, sub := range subscriptions { + sub.close() + } + }(e.sessionSubscriptions) + // Can't use clear(...) here as the maps are processed asynchronously by the + // goroutines above. + e.backendRoomSubscriptions = make(map[string]*asyncBackendRoomSubscriberNats) + e.roomSubscriptions = make(map[string]*asyncRoomSubscriberNats) + e.userSubscriptions = make(map[string]*asyncUserSubscriberNats) + e.sessionSubscriptions = make(map[string]*asyncSessionSubscriberNats) + wg.Wait() + e.client.Close() +} + +func (e *asyncEventsNats) RegisterBackendRoomListener(roomId string, backend *Backend, listener AsyncBackendRoomEventListener) error { + key := GetSubjectForBackendRoomId(roomId, backend) + + e.mu.Lock() + defer e.mu.Unlock() + sub, found := e.backendRoomSubscriptions[key] + if !found { + var err error + if sub, err = newAsyncBackendRoomSubscriberNats(key, e.client); err != nil { + return err + } + + e.backendRoomSubscriptions[key] = sub + } + sub.addListener(listener) + return nil +} + +func (e *asyncEventsNats) UnregisterBackendRoomListener(roomId string, backend *Backend, listener AsyncBackendRoomEventListener) { + key := GetSubjectForBackendRoomId(roomId, backend) + + e.mu.Lock() + defer e.mu.Unlock() + sub, found := e.backendRoomSubscriptions[key] + if !found { + return + } + + if !sub.removeListener(listener) { + delete(e.backendRoomSubscriptions, key) + sub.close() + } +} + +func (e *asyncEventsNats) RegisterRoomListener(roomId string, backend *Backend, listener AsyncRoomEventListener) error { + key := GetSubjectForRoomId(roomId, backend) + + e.mu.Lock() + defer e.mu.Unlock() + sub, found := e.roomSubscriptions[key] + if !found { + var err error + if sub, err = newAsyncRoomSubscriberNats(key, e.client); err != nil { + return err + } + + e.roomSubscriptions[key] = sub + } + sub.addListener(listener) + return nil +} + +func (e *asyncEventsNats) UnregisterRoomListener(roomId string, backend *Backend, listener AsyncRoomEventListener) { + key := GetSubjectForRoomId(roomId, backend) + + e.mu.Lock() + defer e.mu.Unlock() + sub, found := e.roomSubscriptions[key] + if !found { + return + } + + if !sub.removeListener(listener) { + delete(e.roomSubscriptions, key) + sub.close() + } +} + +func (e *asyncEventsNats) RegisterUserListener(roomId string, backend *Backend, listener AsyncUserEventListener) error { + key := GetSubjectForUserId(roomId, backend) + + e.mu.Lock() + defer e.mu.Unlock() + sub, found := e.userSubscriptions[key] + if !found { + var err error + if sub, err = newAsyncUserSubscriberNats(key, e.client); err != nil { + return err + } + + e.userSubscriptions[key] = sub + } + sub.addListener(listener) + return nil +} + +func (e *asyncEventsNats) UnregisterUserListener(roomId string, backend *Backend, listener AsyncUserEventListener) { + key := GetSubjectForUserId(roomId, backend) + + e.mu.Lock() + defer e.mu.Unlock() + sub, found := e.userSubscriptions[key] + if !found { + return + } + + if !sub.removeListener(listener) { + delete(e.userSubscriptions, key) + sub.close() + } +} + +func (e *asyncEventsNats) RegisterSessionListener(sessionId string, backend *Backend, listener AsyncSessionEventListener) error { + key := GetSubjectForSessionId(sessionId, backend) + + e.mu.Lock() + defer e.mu.Unlock() + sub, found := e.sessionSubscriptions[key] + if !found { + var err error + if sub, err = newAsyncSessionSubscriberNats(key, e.client); err != nil { + return err + } + + e.sessionSubscriptions[key] = sub + } + sub.addListener(listener) + return nil +} + +func (e *asyncEventsNats) UnregisterSessionListener(sessionId string, backend *Backend, listener AsyncSessionEventListener) { + key := GetSubjectForSessionId(sessionId, backend) + + e.mu.Lock() + defer e.mu.Unlock() + sub, found := e.sessionSubscriptions[key] + if !found { + return + } + + if !sub.removeListener(listener) { + delete(e.sessionSubscriptions, key) + sub.close() + } +} + +func (e *asyncEventsNats) publish(subject string, message *AsyncMessage) error { + message.SendTime = time.Now() + return e.client.Publish(subject, message) +} + +func (e *asyncEventsNats) PublishBackendRoomMessage(roomId string, backend *Backend, message *AsyncMessage) error { + subject := GetSubjectForBackendRoomId(roomId, backend) + return e.publish(subject, message) +} + +func (e *asyncEventsNats) PublishRoomMessage(roomId string, backend *Backend, message *AsyncMessage) error { + subject := GetSubjectForRoomId(roomId, backend) + return e.publish(subject, message) +} + +func (e *asyncEventsNats) PublishUserMessage(userId string, backend *Backend, message *AsyncMessage) error { + subject := GetSubjectForUserId(userId, backend) + return e.publish(subject, message) +} + +func (e *asyncEventsNats) PublishSessionMessage(sessionId string, backend *Backend, message *AsyncMessage) error { + subject := GetSubjectForSessionId(sessionId, backend) + return e.publish(subject, message) +} diff --git a/async_events_test.go b/async_events_test.go new file mode 100644 index 0000000..b72a30a --- /dev/null +++ b/async_events_test.go @@ -0,0 +1,75 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + eventBackendsForTest = []string{ + "loopback", + "nats", + } +) + +func getAsyncEventsForTest(t *testing.T) AsyncEvents { + var events AsyncEvents + if strings.HasSuffix(t.Name(), "/nats") { + events = getRealAsyncEventsForTest(t) + } else { + events = getLoopbackAsyncEventsForTest(t) + } + t.Cleanup(func() { + events.Close() + }) + return events +} + +func getRealAsyncEventsForTest(t *testing.T) AsyncEvents { + url := startLocalNatsServer(t) + events, err := NewAsyncEvents(url) + if err != nil { + require.NoError(t, err) + } + return events +} + +func getLoopbackAsyncEventsForTest(t *testing.T) AsyncEvents { + events, err := NewAsyncEvents(NatsLoopbackUrl) + if err != nil { + require.NoError(t, err) + } + + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + nats := (events.(*asyncEventsNats)).client + (nats).(*LoopbackNatsClient).waitForSubscriptionsEmpty(ctx, t) + }) + return events +} diff --git a/backend_client.go b/backend_client.go new file mode 100644 index 0000000..8b60869 --- /dev/null +++ b/backend_client.go @@ -0,0 +1,217 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" + + "github.com/dlintw/goconf" +) + +var ( + ErrNotRedirecting = errors.New("not redirecting to different host") + ErrUnsupportedContentType = errors.New("unsupported_content_type") + + ErrIncompleteResponse = errors.New("incomplete OCS response") + ErrThrottledResponse = errors.New("throttled OCS response") +) + +type BackendClient struct { + hub *Hub + version string + backends *BackendConfiguration + + pool *HttpClientPool + capabilities *Capabilities +} + +func NewBackendClient(config *goconf.ConfigFile, maxConcurrentRequestsPerHost int, version string, etcdClient *EtcdClient) (*BackendClient, error) { + backends, err := NewBackendConfiguration(config, etcdClient) + if err != nil { + return nil, err + } + + skipverify, _ := config.GetBool("backend", "skipverify") + if skipverify { + log.Println("WARNING: Backend verification is disabled!") + } + + pool, err := NewHttpClientPool(maxConcurrentRequestsPerHost, skipverify) + if err != nil { + return nil, err + } + + capabilities, err := NewCapabilities(version, pool) + if err != nil { + return nil, err + } + + return &BackendClient{ + version: version, + backends: backends, + + pool: pool, + capabilities: capabilities, + }, nil +} + +func (b *BackendClient) Close() { + b.backends.Close() +} + +func (b *BackendClient) Reload(config *goconf.ConfigFile) { + b.backends.Reload(config) +} + +func (b *BackendClient) GetCompatBackend() *Backend { + return b.backends.GetCompatBackend() +} + +func (b *BackendClient) GetBackend(u *url.URL) *Backend { + return b.backends.GetBackend(u) +} + +func (b *BackendClient) GetBackends() []*Backend { + return b.backends.GetBackends() +} + +func (b *BackendClient) IsUrlAllowed(u *url.URL) bool { + return b.backends.IsUrlAllowed(u) +} + +func isOcsRequest(u *url.URL) bool { + return strings.Contains(u.Path, "/ocs/v2.php") || strings.Contains(u.Path, "/ocs/v1.php") +} + +// PerformJSONRequest sends a JSON POST request to the given url and decodes +// the result into "response". +func (b *BackendClient) PerformJSONRequest(ctx context.Context, u *url.URL, request interface{}, response interface{}) error { + if u == nil { + return fmt.Errorf("no url passed to perform JSON request %+v", request) + } + + secret := b.backends.GetSecret(u) + if secret == nil { + return fmt.Errorf("no backend secret configured for for %s", u) + } + + var requestUrl *url.URL + if b.capabilities.HasCapabilityFeature(ctx, u, FeatureSignalingV3Api) { + newUrl := *u + newUrl.Path = strings.Replace(newUrl.Path, "/spreed/api/v1/signaling/", "/spreed/api/v3/signaling/", -1) + newUrl.Path = strings.Replace(newUrl.Path, "/spreed/api/v2/signaling/", "/spreed/api/v3/signaling/", -1) + requestUrl = &newUrl + } else { + requestUrl = u + } + + c, pool, err := b.pool.Get(ctx, u) + if err != nil { + log.Printf("Could not get client for host %s: %s", u.Host, err) + return err + } + defer pool.Put(c) + + data, err := json.Marshal(request) + if err != nil { + log.Printf("Could not marshal request %+v: %s", request, err) + return err + } + + req, err := http.NewRequestWithContext(ctx, "POST", requestUrl.String(), bytes.NewReader(data)) + if err != nil { + log.Printf("Could not create request to %s: %s", requestUrl, err) + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("OCS-APIRequest", "true") + req.Header.Set("User-Agent", "nextcloud-spreed-signaling/"+b.version) + if b.hub != nil { + req.Header.Set("X-Spreed-Signaling-Features", strings.Join(b.hub.info.Features, ", ")) + } + + // Add checksum so the backend can validate the request. + AddBackendChecksum(req, data, secret) + + resp, err := c.Do(req) + if err != nil { + log.Printf("Could not send request %s to %s: %s", string(data), req.URL, err) + return err + } + defer resp.Body.Close() + + ct := resp.Header.Get("Content-Type") + if !strings.HasPrefix(ct, "application/json") { + log.Printf("Received unsupported content-type from %s: %s (%s)", req.URL, ct, resp.Status) + return ErrUnsupportedContentType + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Could not read response body from %s: %s", req.URL, err) + return err + } + + if isOcsRequest(u) || req.Header.Get("OCS-APIRequest") != "" { + // OCS response are wrapped in an OCS container that needs to be parsed + // to get the actual contents: + // { + // "ocs": { + // "meta": { ... }, + // "data": { ... } + // } + // } + var ocs OcsResponse + if err := json.Unmarshal(body, &ocs); err != nil { + log.Printf("Could not decode OCS response %s from %s: %s", string(body), req.URL, err) + return err + } else if ocs.Ocs == nil || len(ocs.Ocs.Data) == 0 { + log.Printf("Incomplete OCS response %s from %s", string(body), req.URL) + return ErrIncompleteResponse + } + + switch ocs.Ocs.Meta.StatusCode { + case http.StatusTooManyRequests: + log.Printf("Throttled OCS response %s from %s", string(body), req.URL) + return ErrThrottledResponse + } + + if err := json.Unmarshal(ocs.Ocs.Data, response); err != nil { + log.Printf("Could not decode OCS response body %s from %s: %s", string(ocs.Ocs.Data), req.URL, err) + return err + } + } else if err := json.Unmarshal(body, response); err != nil { + log.Printf("Could not decode response body %s from %s: %s", string(body), req.URL, err) + return err + } + return nil +} diff --git a/talk/backend_client_test.go b/backend_client_test.go similarity index 84% rename from talk/backend_client_test.go rename to backend_client_test.go index 8443c78..d0a4d77 100644 --- a/talk/backend_client_test.go +++ b/backend_client_test.go @@ -19,9 +19,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( + "context" "encoding/json" "io" "net/http" @@ -34,10 +35,6 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/pool" ) func returnOCS(t *testing.T, w http.ResponseWriter, body []byte) { @@ -70,21 +67,19 @@ func returnOCS(t *testing.T, w http.ResponseWriter, body []byte) { func TestPostOnRedirect(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) r := mux.NewRouter() r.HandleFunc("/ocs/v2.php/one", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/ocs/v2.php/two", http.StatusTemporaryRedirect) }) r.HandleFunc("/ocs/v2.php/two", func(w http.ResponseWriter, r *http.Request) { - assert := assert.New(t) body, err := io.ReadAll(r.Body) - assert.NoError(err) + require.NoError(err) var request map[string]string err = json.Unmarshal(body, &request) - assert.NoError(err) + require.NoError(err) returnOCS(t, w, body) }) @@ -101,9 +96,10 @@ func TestPostOnRedirect(t *testing.T) { if u.Scheme == "http" { config.AddOption("backend", "allowhttp", "true") } - client, err := NewBackendClient(ctx, config, 1, "0.0", nil) + client, err := NewBackendClient(config, 1, "0.0", nil) require.NoError(err) + ctx := context.Background() request := map[string]string{ "foo": "bar", } @@ -118,8 +114,7 @@ func TestPostOnRedirect(t *testing.T) { func TestPostOnRedirectDifferentHost(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) r := mux.NewRouter() r.HandleFunc("/ocs/v2.php/one", func(w http.ResponseWriter, r *http.Request) { @@ -137,9 +132,10 @@ func TestPostOnRedirectDifferentHost(t *testing.T) { if u.Scheme == "http" { config.AddOption("backend", "allowhttp", "true") } - client, err := NewBackendClient(ctx, config, 1, "0.0", nil) + client, err := NewBackendClient(config, 1, "0.0", nil) require.NoError(err) + ctx := context.Background() request := map[string]string{ "foo": "bar", } @@ -147,7 +143,7 @@ func TestPostOnRedirectDifferentHost(t *testing.T) { err = client.PerformJSONRequest(ctx, u, request, &response) if err != nil { // The redirect to a different host should have failed. - require.ErrorIs(err, pool.ErrNotRedirecting) + require.ErrorIs(err, ErrNotRedirecting) } else { require.Fail("The redirect should have failed") } @@ -155,8 +151,7 @@ func TestPostOnRedirectDifferentHost(t *testing.T) { func TestPostOnRedirectStatusFound(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) r := mux.NewRouter() @@ -165,10 +160,9 @@ func TestPostOnRedirectStatusFound(t *testing.T) { }) r.HandleFunc("/ocs/v2.php/two", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) - if assert.NoError(err) { - assert.Empty(string(body), "Should not have received any body, got %s", string(body)) - } + require.NoError(err) + assert.Empty(string(body), "Should not have received any body, got %s", string(body)) returnOCS(t, w, []byte("{}")) }) server := httptest.NewServer(r) @@ -183,9 +177,10 @@ func TestPostOnRedirectStatusFound(t *testing.T) { if u.Scheme == "http" { config.AddOption("backend", "allowhttp", "true") } - client, err := NewBackendClient(ctx, config, 1, "0.0", nil) + client, err := NewBackendClient(config, 1, "0.0", nil) require.NoError(err) + ctx := context.Background() request := map[string]string{ "foo": "bar", } @@ -198,8 +193,7 @@ func TestPostOnRedirectStatusFound(t *testing.T) { func TestHandleThrottled(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) r := mux.NewRouter() @@ -218,9 +212,10 @@ func TestHandleThrottled(t *testing.T) { if u.Scheme == "http" { config.AddOption("backend", "allowhttp", "true") } - client, err := NewBackendClient(ctx, config, 1, "0.0", nil) + client, err := NewBackendClient(config, 1, "0.0", nil) require.NoError(err) + ctx := context.Background() request := map[string]string{ "foo": "bar", } diff --git a/talk/backend_configuration.go b/backend_configuration.go similarity index 58% rename from talk/backend_configuration.go rename to backend_configuration.go index e76785e..fa6b45b 100644 --- a/talk/backend_configuration.go +++ b/backend_configuration.go @@ -19,20 +19,15 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "fmt" "net/url" - "slices" "strings" "sync" "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) const ( @@ -42,28 +37,111 @@ const ( DefaultBackendType = BackendTypeStatic ) +var ( + SessionLimitExceeded = NewError("session_limit_exceeded", "Too many sessions connected for this backend.") +) + +type Backend struct { + id string + url string + parsedUrl *url.URL + secret []byte + compat bool + + allowHttp bool + + maxStreamBitrate int + maxScreenBitrate int + + sessionLimit uint64 + sessionsLock sync.Mutex + sessions map[string]bool +} + +func (b *Backend) Id() string { + return b.id +} + +func (b *Backend) Secret() []byte { + return b.secret +} + +func (b *Backend) IsCompat() bool { + return b.compat +} + +func (b *Backend) IsUrlAllowed(u *url.URL) bool { + switch u.Scheme { + case "https": + return true + case "http": + return b.allowHttp + default: + return false + } +} + +func (b *Backend) Url() string { + return b.url +} + +func (b *Backend) ParsedUrl() *url.URL { + return b.parsedUrl +} + +func (b *Backend) Limit() int { + return int(b.sessionLimit) +} + +func (b *Backend) Len() int { + b.sessionsLock.Lock() + defer b.sessionsLock.Unlock() + return len(b.sessions) +} + +func (b *Backend) AddSession(session Session) error { + if session.ClientType() == HelloClientTypeInternal || session.ClientType() == HelloClientTypeVirtual { + // Internal and virtual sessions are not counting to the limit. + return nil + } + + if b.sessionLimit == 0 { + // Not limited + return nil + } + + b.sessionsLock.Lock() + defer b.sessionsLock.Unlock() + if b.sessions == nil { + b.sessions = make(map[string]bool) + } else if uint64(len(b.sessions)) >= b.sessionLimit { + statsBackendLimitExceededTotal.WithLabelValues(b.id).Inc() + return SessionLimitExceeded + } + + b.sessions[session.PublicId()] = true + return nil +} + +func (b *Backend) RemoveSession(session Session) { + b.sessionsLock.Lock() + defer b.sessionsLock.Unlock() + + delete(b.sessions, session.PublicId()) +} + type BackendStorage interface { Close() - Reload(cfg *goconf.ConfigFile) + Reload(config *goconf.ConfigFile) GetCompatBackend() *Backend GetBackend(u *url.URL) *Backend GetBackends() []*Backend } -type BackendStorageStats interface { - AddBackends(count int) - RemoveBackends(count int) - IncBackends() - DecBackends() -} - type backendStorageCommon struct { - mu sync.RWMutex - // +checklocks:mu + mu sync.RWMutex backends map[string][]*Backend - - stats BackendStorageStats // +checklocksignore: Only written to from constructor } func (s *backendStorageCommon) GetBackends() []*Backend { @@ -74,12 +152,6 @@ func (s *backendStorageCommon) GetBackends() []*Backend { for _, entries := range s.backends { result = append(result, entries...) } - slices.SortFunc(result, func(a, b *Backend) int { - return strings.Compare(a.Id(), b.Id()) - }) - result = slices.CompactFunc(result, func(a, b *Backend) bool { - return a.Id() == b.Id() - }) return result } @@ -101,7 +173,10 @@ func (s *backendStorageCommon) getBackendLocked(u *url.URL) *Backend { continue } - if entry.HasUrl(url) { + if entry.url == "" { + // Old-style configuration, only hosts are configured. + return entry + } else if strings.HasPrefix(url, entry.url) { return entry } } @@ -113,50 +188,21 @@ type BackendConfiguration struct { storage BackendStorage } -type prometheusBackendStats struct{} - -func (s *prometheusBackendStats) AddBackends(count int) { - statsBackendsCurrent.Add(float64(count)) -} - -func (s *prometheusBackendStats) RemoveBackends(count int) { - statsBackendsCurrent.Sub(float64(count)) -} - -func (s *prometheusBackendStats) IncBackends() { - statsBackendsCurrent.Inc() -} - -func (s *prometheusBackendStats) DecBackends() { - statsBackendsCurrent.Dec() -} - -var ( - defaultBackendStats = &prometheusBackendStats{} -) - -func NewBackendConfiguration(logger log.Logger, config *goconf.ConfigFile, etcdClient etcd.Client) (*BackendConfiguration, error) { - return NewBackendConfigurationWithStats(logger, config, etcdClient, nil) -} - -func NewBackendConfigurationWithStats(logger log.Logger, config *goconf.ConfigFile, etcdClient etcd.Client, stats BackendStorageStats) (*BackendConfiguration, error) { +func NewBackendConfiguration(config *goconf.ConfigFile, etcdClient *EtcdClient) (*BackendConfiguration, error) { backendType, _ := config.GetString("backend", "backendtype") if backendType == "" { backendType = DefaultBackendType } - if stats == nil { - RegisterBackendConfigurationStats() - stats = defaultBackendStats - } + RegisterBackendConfigurationStats() var storage BackendStorage var err error switch backendType { case BackendTypeStatic: - storage, err = NewBackendStorageStatic(logger, config, stats) + storage, err = NewBackendStorageStatic(config) case BackendTypeEtcd: - storage, err = NewBackendStorageEtcd(logger, config, etcdClient, stats) + storage, err = NewBackendStorageEtcd(config, etcdClient) default: err = fmt.Errorf("unknown backend type: %s", backendType) } @@ -182,7 +228,10 @@ func (b *BackendConfiguration) GetCompatBackend() *Backend { } func (b *BackendConfiguration) GetBackend(u *url.URL) *Backend { - u, _ = internal.CanonicalizeUrl(u) + if strings.Contains(u.Host, ":") && hasStandardPort(u) { + u.Host = u.Hostname() + } + return b.storage.GetBackend(u) } diff --git a/talk/backend_stats_prometheus.go b/backend_configuration_stats_prometheus.go similarity index 67% rename from talk/backend_stats_prometheus.go rename to backend_configuration_stats_prometheus.go index f335df4..d19d7f9 100644 --- a/talk/backend_stats_prometheus.go +++ b/backend_configuration_stats_prometheus.go @@ -19,12 +19,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" ) var ( @@ -34,19 +32,38 @@ var ( Name: "session_limit", Help: "The session limit of a backend", }, []string{"backend"}) - statsBackendLimitExceededTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ // +checklocksignore: Global readonly variable. + statsBackendLimitExceededTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "signaling", Subsystem: "backend", Name: "session_limit_exceeded_total", Help: "The number of times the session limit exceeded", }, []string{"backend"}) + statsBackendsCurrent = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "signaling", + Subsystem: "backend", + Name: "current", + Help: "The current number of configured backends", + }) - backendStats = []prometheus.Collector{ + backendConfigurationStats = []prometheus.Collector{ statsBackendLimit, statsBackendLimitExceededTotal, + statsBackendsCurrent, } ) -func registerBackendStats() { - metrics.RegisterAll(backendStats...) +func RegisterBackendConfigurationStats() { + registerAll(backendConfigurationStats...) +} + +func updateBackendStats(backend *Backend) { + if backend.sessionLimit > 0 { + statsBackendLimit.WithLabelValues(backend.id).Set(float64(backend.sessionLimit)) + } else { + statsBackendLimit.DeleteLabelValues(backend.id) + } +} + +func deleteBackendStats(backend *Backend) { + statsBackendLimit.DeleteLabelValues(backend.id) } diff --git a/talk/backend_configuration_test.go b/backend_configuration_test.go similarity index 56% rename from talk/backend_configuration_test.go rename to backend_configuration_test.go index 83f2f15..e467cbd 100644 --- a/talk/backend_configuration_test.go +++ b/backend_configuration_test.go @@ -19,33 +19,25 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "context" "net/url" "reflect" - "slices" - "strings" + "sort" "testing" "github.com/dlintw/goconf" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" -) - -var ( - testBackendSecret = []byte("secret") ) func testUrls(t *testing.T, config *BackendConfiguration, valid_urls []string, invalid_urls []string) { for _, u := range valid_urls { + u := u t.Run(u, func(t *testing.T) { - t.Parallel() assert := assert.New(t) parsed, err := url.ParseRequestURI(u) if !assert.NoError(err, "The url %s should be valid", u) { @@ -57,8 +49,8 @@ func testUrls(t *testing.T, config *BackendConfiguration, valid_urls []string, i }) } for _, u := range invalid_urls { + u := u t.Run(u, func(t *testing.T) { - t.Parallel() assert := assert.New(t) parsed, _ := url.ParseRequestURI(u) assert.False(config.IsUrlAllowed(parsed), "The url %s should not be allowed", u) @@ -68,8 +60,8 @@ func testUrls(t *testing.T, config *BackendConfiguration, valid_urls []string, i func testBackends(t *testing.T, config *BackendConfiguration, valid_urls [][]string, invalid_urls []string) { for _, entry := range valid_urls { + entry := entry t.Run(entry[0], func(t *testing.T) { - t.Parallel() assert := assert.New(t) u := entry[0] parsed, err := url.ParseRequestURI(u) @@ -83,8 +75,8 @@ func testBackends(t *testing.T, config *BackendConfiguration, valid_urls [][]str }) } for _, u := range invalid_urls { + u := u t.Run(u, func(t *testing.T) { - t.Parallel() assert := assert.New(t) parsed, _ := url.ParseRequestURI(u) assert.False(config.IsUrlAllowed(parsed), "The url %s should not be allowed", u) @@ -92,29 +84,8 @@ func testBackends(t *testing.T, config *BackendConfiguration, valid_urls [][]str } } -type mockBackendStats struct { - value int -} - -func (s *mockBackendStats) AddBackends(count int) { - s.value += count -} - -func (s *mockBackendStats) RemoveBackends(count int) { - s.value -= count -} - -func (s *mockBackendStats) IncBackends() { - s.value++ -} - -func (s *mockBackendStats) DecBackends() { - s.value-- -} - func TestIsUrlAllowed_Compat(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) // Old-style configuration valid_urls := []string{ "http://domain.invalid", @@ -129,14 +100,13 @@ func TestIsUrlAllowed_Compat(t *testing.T) { config.AddOption("backend", "allowed", "domain.invalid") config.AddOption("backend", "allowhttp", "true") config.AddOption("backend", "secret", string(testBackendSecret)) - cfg, err := NewBackendConfiguration(logger, config, nil) + cfg, err := NewBackendConfiguration(config, nil) require.NoError(t, err) testUrls(t, cfg, valid_urls, invalid_urls) } func TestIsUrlAllowed_CompatForceHttps(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) // Old-style configuration, force HTTPS valid_urls := []string{ "https://domain.invalid", @@ -150,14 +120,13 @@ func TestIsUrlAllowed_CompatForceHttps(t *testing.T) { config := goconf.NewConfigFile() config.AddOption("backend", "allowed", "domain.invalid") config.AddOption("backend", "secret", string(testBackendSecret)) - cfg, err := NewBackendConfiguration(logger, config, nil) + cfg, err := NewBackendConfiguration(config, nil) require.NoError(t, err) testUrls(t, cfg, valid_urls, invalid_urls) } func TestIsUrlAllowed(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) valid_urls := [][]string{ {"https://domain.invalid/foo", string(testBackendSecret) + "-foo"}, {"https://domain.invalid/foo/", string(testBackendSecret) + "-foo"}, @@ -195,14 +164,13 @@ func TestIsUrlAllowed(t *testing.T) { config.AddOption("baz", "secret", string(testBackendSecret)+"-baz") config.AddOption("lala", "url", "https://otherdomain.invalid/") config.AddOption("lala", "secret", string(testBackendSecret)+"-lala") - cfg, err := NewBackendConfiguration(logger, config, nil) + cfg, err := NewBackendConfiguration(config, nil) require.NoError(t, err) testBackends(t, cfg, valid_urls, invalid_urls) } func TestIsUrlAllowed_EmptyAllowlist(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) valid_urls := []string{} invalid_urls := []string{ "http://domain.invalid", @@ -212,14 +180,13 @@ func TestIsUrlAllowed_EmptyAllowlist(t *testing.T) { config := goconf.NewConfigFile() config.AddOption("backend", "allowed", "") config.AddOption("backend", "secret", string(testBackendSecret)) - cfg, err := NewBackendConfiguration(logger, config, nil) + cfg, err := NewBackendConfiguration(config, nil) require.NoError(t, err) testUrls(t, cfg, valid_urls, invalid_urls) } func TestIsUrlAllowed_AllowAll(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) valid_urls := []string{ "http://domain.invalid", "https://domain.invalid", @@ -232,7 +199,7 @@ func TestIsUrlAllowed_AllowAll(t *testing.T) { config.AddOption("backend", "allowall", "true") config.AddOption("backend", "allowed", "") config.AddOption("backend", "secret", string(testBackendSecret)) - cfg, err := NewBackendConfiguration(logger, config, nil) + cfg, err := NewBackendConfiguration(config, nil) require.NoError(t, err) testUrls(t, cfg, valid_urls, invalid_urls) } @@ -243,7 +210,7 @@ type ParseBackendIdsTestcase struct { } func TestParseBackendIds(t *testing.T) { - t.Parallel() + CatchLogForTest(t) testcases := []ParseBackendIdsTestcase{ {"", nil}, {"backend1", []string{"backend1"}}, @@ -262,12 +229,9 @@ func TestParseBackendIds(t *testing.T) { } func TestBackendReloadNoChange(t *testing.T) { - t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) require := require.New(t) - assert := assert.New(t) + current := testutil.ToFloat64(statsBackendsCurrent) original_config := goconf.NewConfigFile() original_config.AddOption("backend", "backends", "backend1, backend2") original_config.AddOption("backend", "allowall", "false") @@ -275,9 +239,9 @@ func TestBackendReloadNoChange(t *testing.T) { original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") original_config.AddOption("backend2", "url", "http://domain2.invalid") original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - o_cfg, err := NewBackendConfigurationWithStats(logger, original_config, nil, stats) + o_cfg, err := NewBackendConfiguration(original_config, nil) require.NoError(err) - assert.Equal(2, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+2) new_config := goconf.NewConfigFile() new_config.AddOption("backend", "backends", "backend1, backend2") @@ -286,24 +250,21 @@ func TestBackendReloadNoChange(t *testing.T) { new_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") new_config.AddOption("backend2", "url", "http://domain2.invalid") new_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - n_cfg, err := NewBackendConfigurationWithStats(logger, new_config, nil, stats) + n_cfg, err := NewBackendConfiguration(new_config, nil) require.NoError(err) - assert.Equal(4, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+4) o_cfg.Reload(original_config) - assert.Equal(4, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+4) if !reflect.DeepEqual(n_cfg, o_cfg) { - assert.Fail("BackendConfiguration should be equal after Reload") + assert.Fail(t, "BackendConfiguration should be equal after Reload") } } func TestBackendReloadChangeExistingURL(t *testing.T) { - t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) require := require.New(t) - assert := assert.New(t) + current := testutil.ToFloat64(statsBackendsCurrent) original_config := goconf.NewConfigFile() original_config.AddOption("backend", "backends", "backend1, backend2") original_config.AddOption("backend", "allowall", "false") @@ -311,10 +272,10 @@ func TestBackendReloadChangeExistingURL(t *testing.T) { original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") original_config.AddOption("backend2", "url", "http://domain2.invalid") original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - o_cfg, err := NewBackendConfigurationWithStats(logger, original_config, nil, stats) + o_cfg, err := NewBackendConfiguration(original_config, nil) require.NoError(err) - assert.Equal(2, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+2) new_config := goconf.NewConfigFile() new_config.AddOption("backend", "backends", "backend1, backend2") new_config.AddOption("backend", "allowall", "false") @@ -323,28 +284,25 @@ func TestBackendReloadChangeExistingURL(t *testing.T) { new_config.AddOption("backend1", "sessionlimit", "10") new_config.AddOption("backend2", "url", "http://domain2.invalid") new_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - n_cfg, err := NewBackendConfigurationWithStats(logger, new_config, nil, stats) + n_cfg, err := NewBackendConfiguration(new_config, nil) require.NoError(err) - assert.Equal(4, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+4) original_config.RemoveOption("backend1", "url") original_config.AddOption("backend1", "url", "http://domain3.invalid") original_config.AddOption("backend1", "sessionlimit", "10") o_cfg.Reload(original_config) - assert.Equal(4, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+4) if !reflect.DeepEqual(n_cfg, o_cfg) { - assert.Fail("BackendConfiguration should be equal after Reload") + assert.Fail(t, "BackendConfiguration should be equal after Reload") } } func TestBackendReloadChangeSecret(t *testing.T) { - t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) require := require.New(t) - assert := assert.New(t) + current := testutil.ToFloat64(statsBackendsCurrent) original_config := goconf.NewConfigFile() original_config.AddOption("backend", "backends", "backend1, backend2") original_config.AddOption("backend", "allowall", "false") @@ -352,10 +310,10 @@ func TestBackendReloadChangeSecret(t *testing.T) { original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") original_config.AddOption("backend2", "url", "http://domain2.invalid") original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - o_cfg, err := NewBackendConfigurationWithStats(logger, original_config, nil, stats) + o_cfg, err := NewBackendConfiguration(original_config, nil) require.NoError(err) - assert.Equal(2, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+2) new_config := goconf.NewConfigFile() new_config.AddOption("backend", "backends", "backend1, backend2") new_config.AddOption("backend", "allowall", "false") @@ -363,34 +321,33 @@ func TestBackendReloadChangeSecret(t *testing.T) { new_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend3") new_config.AddOption("backend2", "url", "http://domain2.invalid") new_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - n_cfg, err := NewBackendConfigurationWithStats(logger, new_config, nil, stats) + n_cfg, err := NewBackendConfiguration(new_config, nil) require.NoError(err) - assert.Equal(4, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+4) original_config.RemoveOption("backend1", "secret") original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend3") o_cfg.Reload(original_config) - assert.Equal(4, stats.value) - assert.Equal(n_cfg, o_cfg, "BackendConfiguration should be equal after Reload") + checkStatsValue(t, statsBackendsCurrent, current+4) + if !reflect.DeepEqual(n_cfg, o_cfg) { + assert.Fail(t, "BackendConfiguration should be equal after Reload") + } } func TestBackendReloadAddBackend(t *testing.T) { - t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) require := require.New(t) - assert := assert.New(t) + current := testutil.ToFloat64(statsBackendsCurrent) original_config := goconf.NewConfigFile() original_config.AddOption("backend", "backends", "backend1") original_config.AddOption("backend", "allowall", "false") original_config.AddOption("backend1", "url", "http://domain1.invalid") original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") - o_cfg, err := NewBackendConfigurationWithStats(logger, original_config, nil, stats) + o_cfg, err := NewBackendConfiguration(original_config, nil) require.NoError(err) - assert.Equal(1, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+1) new_config := goconf.NewConfigFile() new_config.AddOption("backend", "backends", "backend1, backend2") new_config.AddOption("backend", "allowall", "false") @@ -399,10 +356,10 @@ func TestBackendReloadAddBackend(t *testing.T) { new_config.AddOption("backend2", "url", "http://domain2.invalid") new_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") new_config.AddOption("backend2", "sessionlimit", "10") - n_cfg, err := NewBackendConfigurationWithStats(logger, new_config, nil, stats) + n_cfg, err := NewBackendConfiguration(new_config, nil) require.NoError(err) - assert.Equal(3, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+3) original_config.RemoveOption("backend", "backends") original_config.AddOption("backend", "backends", "backend1, backend2") original_config.AddOption("backend2", "url", "http://domain2.invalid") @@ -410,19 +367,16 @@ func TestBackendReloadAddBackend(t *testing.T) { original_config.AddOption("backend2", "sessionlimit", "10") o_cfg.Reload(original_config) - assert.Equal(4, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+4) if !reflect.DeepEqual(n_cfg, o_cfg) { - assert.Fail("BackendConfiguration should be equal after Reload") + assert.Fail(t, "BackendConfiguration should be equal after Reload") } } func TestBackendReloadRemoveHost(t *testing.T) { - t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) require := require.New(t) - assert := assert.New(t) + current := testutil.ToFloat64(statsBackendsCurrent) original_config := goconf.NewConfigFile() original_config.AddOption("backend", "backends", "backend1, backend2") original_config.AddOption("backend", "allowall", "false") @@ -430,37 +384,34 @@ func TestBackendReloadRemoveHost(t *testing.T) { original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") original_config.AddOption("backend2", "url", "http://domain2.invalid") original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - o_cfg, err := NewBackendConfigurationWithStats(logger, original_config, nil, stats) + o_cfg, err := NewBackendConfiguration(original_config, nil) require.NoError(err) - assert.Equal(2, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+2) new_config := goconf.NewConfigFile() new_config.AddOption("backend", "backends", "backend1") new_config.AddOption("backend", "allowall", "false") new_config.AddOption("backend1", "url", "http://domain1.invalid") new_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") - n_cfg, err := NewBackendConfigurationWithStats(logger, new_config, nil, stats) + n_cfg, err := NewBackendConfiguration(new_config, nil) require.NoError(err) - assert.Equal(3, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+3) original_config.RemoveOption("backend", "backends") original_config.AddOption("backend", "backends", "backend1") original_config.RemoveSection("backend2") o_cfg.Reload(original_config) - assert.Equal(2, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+2) if !reflect.DeepEqual(n_cfg, o_cfg) { - assert.Fail("BackendConfiguration should be equal after Reload") + assert.Fail(t, "BackendConfiguration should be equal after Reload") } } func TestBackendReloadRemoveBackendFromSharedHost(t *testing.T) { - t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) require := require.New(t) - assert := assert.New(t) + current := testutil.ToFloat64(statsBackendsCurrent) original_config := goconf.NewConfigFile() original_config.AddOption("backend", "backends", "backend1, backend2") original_config.AddOption("backend", "allowall", "false") @@ -468,34 +419,36 @@ func TestBackendReloadRemoveBackendFromSharedHost(t *testing.T) { original_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") original_config.AddOption("backend2", "url", "http://domain1.invalid/bar/") original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - o_cfg, err := NewBackendConfigurationWithStats(logger, original_config, nil, stats) + o_cfg, err := NewBackendConfiguration(original_config, nil) require.NoError(err) - assert.Equal(2, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+2) new_config := goconf.NewConfigFile() new_config.AddOption("backend", "backends", "backend1") new_config.AddOption("backend", "allowall", "false") new_config.AddOption("backend1", "url", "http://domain1.invalid/foo/") new_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") - n_cfg, err := NewBackendConfigurationWithStats(logger, new_config, nil, stats) + n_cfg, err := NewBackendConfiguration(new_config, nil) require.NoError(err) - assert.Equal(3, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+3) original_config.RemoveOption("backend", "backends") original_config.AddOption("backend", "backends", "backend1") original_config.RemoveSection("backend2") o_cfg.Reload(original_config) - assert.Equal(2, stats.value) + checkStatsValue(t, statsBackendsCurrent, current+2) if !reflect.DeepEqual(n_cfg, o_cfg) { - assert.Fail("BackendConfiguration should be equal after Reload") + assert.Fail(t, "BackendConfiguration should be equal after Reload") } } func sortBackends(backends []*Backend) []*Backend { - result := slices.Clone(backends) - slices.SortFunc(result, func(a, b *Backend) int { - return strings.Compare(a.Id(), b.Id()) + result := make([]*Backend, len(backends)) + copy(result, backends) + + sort.Slice(result, func(i, j int) bool { + return result[i].Id() < result[j].Id() }) return result } @@ -508,26 +461,24 @@ func mustParse(s string) *url.URL { return p } -func TestBackendConfiguration_EtcdCompat(t *testing.T) { +func TestBackendConfiguration_Etcd(t *testing.T) { t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) - embedEtcd, client := etcdtest.NewClientForTest(t) + etcd, client := NewEtcdClientForTest(t) url1 := "https://domain1.invalid/foo" initialSecret1 := string(testBackendSecret) + "-backend1-initial" secret1 := string(testBackendSecret) + "-backend1" - embedEtcd.SetValue("/backends/1_one", []byte("{\"url\":\""+url1+"\",\"secret\":\""+initialSecret1+"\"}")) + SetEtcdValue(etcd, "/backends/1_one", []byte("{\"url\":\""+url1+"\",\"secret\":\""+initialSecret1+"\"}")) config := goconf.NewConfigFile() config.AddOption("backend", "backendtype", "etcd") config.AddOption("backend", "backendprefix", "/backends") - cfg, err := NewBackendConfigurationWithStats(logger, config, client, stats) + cfg, err := NewBackendConfiguration(config, client) require.NoError(err) defer cfg.Close() @@ -540,96 +491,90 @@ func TestBackendConfiguration_EtcdCompat(t *testing.T) { require.NoError(storage.WaitForInitialized(ctx)) if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 1) && - assert.Equal([]string{url1}, backends[0].Urls()) && - assert.Equal(initialSecret1, string(backends[0].Secret())) { - if backend := cfg.GetBackend(mustParse(url1)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) + assert.Equal(url1, backends[0].url) && + assert.Equal(initialSecret1, string(backends[0].secret)) { + if backend := cfg.GetBackend(mustParse(url1)); backend != backends[0] { + assert.Fail("Expected backend %+v, got %+v", backends[0], backend) } } - test.DrainWakeupChannel(ch) - embedEtcd.SetValue("/backends/1_one", []byte("{\"url\":\""+url1+"\",\"secret\":\""+secret1+"\"}")) + drainWakeupChannel(ch) + SetEtcdValue(etcd, "/backends/1_one", []byte("{\"url\":\""+url1+"\",\"secret\":\""+secret1+"\"}")) <-ch - assert.Equal(1, stats.value) if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 1) && - assert.Equal([]string{url1}, backends[0].Urls()) && - assert.Equal(secret1, string(backends[0].Secret())) { - if backend := cfg.GetBackend(mustParse(url1)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) + assert.Equal(url1, backends[0].url) && + assert.Equal(secret1, string(backends[0].secret)) { + if backend := cfg.GetBackend(mustParse(url1)); backend != backends[0] { + assert.Fail("Expected backend %+v, got %+v", backends[0], backend) } } url2 := "https://domain1.invalid/bar" secret2 := string(testBackendSecret) + "-backend2" - test.DrainWakeupChannel(ch) - embedEtcd.SetValue("/backends/2_two", []byte("{\"url\":\""+url2+"\",\"secret\":\""+secret2+"\"}")) + drainWakeupChannel(ch) + SetEtcdValue(etcd, "/backends/2_two", []byte("{\"url\":\""+url2+"\",\"secret\":\""+secret2+"\"}")) <-ch - assert.Equal(2, stats.value) if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 2) && - assert.Equal([]string{url1}, backends[0].Urls()) && - assert.Equal(secret1, string(backends[0].Secret())) && - assert.Equal([]string{url2}, backends[1].Urls()) && - assert.Equal(secret2, string(backends[1].Secret())) { - if backend := cfg.GetBackend(mustParse(url1)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) - } else if backend := cfg.GetBackend(mustParse(url2)); assert.NotNil(backend) { - assert.Equal(backends[1], backend) + assert.Equal(url1, backends[0].url) && + assert.Equal(secret1, string(backends[0].secret)) && + assert.Equal(url2, backends[1].url) && + assert.Equal(secret2, string(backends[1].secret)) { + if backend := cfg.GetBackend(mustParse(url1)); backend != backends[0] { + assert.Fail("Expected backend %+v, got %+v", backends[0], backend) + } else if backend := cfg.GetBackend(mustParse(url2)); backend != backends[1] { + assert.Fail("Expected backend %+v, got %+v", backends[1], backend) } } url3 := "https://domain2.invalid/foo" secret3 := string(testBackendSecret) + "-backend3" - test.DrainWakeupChannel(ch) - embedEtcd.SetValue("/backends/3_three", []byte("{\"url\":\""+url3+"\",\"secret\":\""+secret3+"\"}")) + drainWakeupChannel(ch) + SetEtcdValue(etcd, "/backends/3_three", []byte("{\"url\":\""+url3+"\",\"secret\":\""+secret3+"\"}")) <-ch - assert.Equal(3, stats.value) if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 3) && - assert.Equal([]string{url1}, backends[0].Urls()) && - assert.Equal(secret1, string(backends[0].Secret())) && - assert.Equal([]string{url2}, backends[1].Urls()) && - assert.Equal(secret2, string(backends[1].Secret())) && - assert.Equal([]string{url3}, backends[2].Urls()) && - assert.Equal(secret3, string(backends[2].Secret())) { - if backend := cfg.GetBackend(mustParse(url1)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) - } else if backend := cfg.GetBackend(mustParse(url2)); assert.NotNil(backend) { - assert.Equal(backends[1], backend) - } else if backend := cfg.GetBackend(mustParse(url3)); assert.NotNil(backend) { - assert.Equal(backends[2], backend) + assert.Equal(url1, backends[0].url) && + assert.Equal(secret1, string(backends[0].secret)) && + assert.Equal(url2, backends[1].url) && + assert.Equal(secret2, string(backends[1].secret)) && + assert.Equal(url3, backends[2].url) && + assert.Equal(secret3, string(backends[2].secret)) { + if backend := cfg.GetBackend(mustParse(url1)); backend != backends[0] { + assert.Fail("Expected backend %+v, got %+v", backends[0], backend) + } else if backend := cfg.GetBackend(mustParse(url2)); backend != backends[1] { + assert.Fail("Expected backend %+v, got %+v", backends[1], backend) + } else if backend := cfg.GetBackend(mustParse(url3)); backend != backends[2] { + assert.Fail("Expected backend %+v, got %+v", backends[2], backend) } } - test.DrainWakeupChannel(ch) - embedEtcd.DeleteValue("/backends/1_one") + drainWakeupChannel(ch) + DeleteEtcdValue(etcd, "/backends/1_one") <-ch - assert.Equal(2, stats.value) if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 2) { - assert.Equal([]string{url2}, backends[0].Urls()) - assert.Equal(secret2, string(backends[0].Secret())) - assert.Equal([]string{url3}, backends[1].Urls()) - assert.Equal(secret3, string(backends[1].Secret())) + assert.Equal(url2, backends[0].url) + assert.Equal(secret2, string(backends[0].secret)) + assert.Equal(url3, backends[1].url) + assert.Equal(secret3, string(backends[1].secret)) } - test.DrainWakeupChannel(ch) - embedEtcd.DeleteValue("/backends/2_two") + drainWakeupChannel(ch) + DeleteEtcdValue(etcd, "/backends/2_two") <-ch - assert.Equal(1, stats.value) if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 1) { - assert.Equal([]string{url3}, backends[0].Urls()) - assert.Equal(secret3, string(backends[0].Secret())) + assert.Equal(url3, backends[0].url) + assert.Equal(secret3, string(backends[0].secret)) } - storage.mu.RLock() - _, found := storage.backends["domain1.invalid"] - storage.mu.RUnlock() - assert.False(found, "Should have removed host information") + if _, found := storage.backends["domain1.invalid"]; found { + assert.Fail("Should have removed host information for %s", "domain1.invalid") + } } func TestBackendCommonSecret(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) u1, err := url.Parse("http://domain1.invalid") @@ -642,7 +587,7 @@ func TestBackendCommonSecret(t *testing.T) { original_config.AddOption("backend1", "url", u1.String()) original_config.AddOption("backend2", "url", u2.String()) original_config.AddOption("backend2", "secret", string(testBackendSecret)+"-backend2") - cfg, err := NewBackendConfiguration(logger, original_config, nil) + cfg, err := NewBackendConfiguration(original_config, nil) require.NoError(err) if b1 := cfg.GetBackend(u1); assert.NotNil(b1) { @@ -667,195 +612,3 @@ func TestBackendCommonSecret(t *testing.T) { assert.Equal(string(testBackendSecret), string(b2.Secret())) } } - -func TestBackendChangeUrls(t *testing.T) { - t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) - require := require.New(t) - assert := assert.New(t) - u1, err := url.Parse("http://domain1.invalid/") - require.NoError(err) - u2, err := url.Parse("http://domain2.invalid/") - require.NoError(err) - original_config := goconf.NewConfigFile() - original_config.AddOption("backend", "backends", "backend1,backend2") - original_config.AddOption("backend", "secret", string(testBackendSecret)) - original_config.AddOption("backend1", "urls", u1.String()) - original_config.AddOption("backend2", "urls", u2.String()) - - cfg, err := NewBackendConfigurationWithStats(logger, original_config, nil, stats) - require.NoError(err) - - assert.Equal(2, stats.value) - if b1 := cfg.GetBackend(u1); assert.NotNil(b1) { - assert.Equal("backend1", b1.Id()) - assert.Equal(string(testBackendSecret), string(b1.Secret())) - assert.Equal([]string{u1.String()}, b1.Urls()) - } - if b2 := cfg.GetBackend(u2); assert.NotNil(b2) { - assert.Equal("backend2", b2.Id()) - assert.Equal(string(testBackendSecret), string(b2.Secret())) - assert.Equal([]string{u2.String()}, b2.Urls()) - } - - // Add url. - updated_config := goconf.NewConfigFile() - updated_config.AddOption("backend", "backends", "backend1") - updated_config.AddOption("backend", "secret", string(testBackendSecret)) - updated_config.AddOption("backend1", "secret", string(testBackendSecret)+"-backend1") - updated_config.AddOption("backend1", "urls", strings.Join([]string{u1.String(), u2.String()}, ",")) - cfg.Reload(updated_config) - - assert.Equal(1, stats.value) - if b1 := cfg.GetBackend(u1); assert.NotNil(b1) { - assert.Equal("backend1", b1.Id()) - assert.Equal(string(testBackendSecret)+"-backend1", string(b1.Secret())) - assert.Equal([]string{u1.String(), u2.String()}, b1.Urls()) - } - if b1 := cfg.GetBackend(u2); assert.NotNil(b1) { - assert.Equal("backend1", b1.Id()) - assert.Equal(string(testBackendSecret)+"-backend1", string(b1.Secret())) - assert.Equal([]string{u1.String(), u2.String()}, b1.Urls()) - } - - // No change reload. - cfg.Reload(updated_config) - assert.Equal(1, stats.value) - if b1 := cfg.GetBackend(u1); assert.NotNil(b1) { - assert.Equal("backend1", b1.Id()) - assert.Equal(string(testBackendSecret)+"-backend1", string(b1.Secret())) - assert.Equal([]string{u1.String(), u2.String()}, b1.Urls()) - } - if b1 := cfg.GetBackend(u2); assert.NotNil(b1) { - assert.Equal("backend1", b1.Id()) - assert.Equal(string(testBackendSecret)+"-backend1", string(b1.Secret())) - assert.Equal([]string{u1.String(), u2.String()}, b1.Urls()) - } - - // Remove url. - updated_config = goconf.NewConfigFile() - updated_config.AddOption("backend", "backends", "backend1") - updated_config.AddOption("backend", "secret", string(testBackendSecret)) - updated_config.AddOption("backend1", "urls", u2.String()) - cfg.Reload(updated_config) - - assert.Equal(1, stats.value) - if b1 := cfg.GetBackend(u2); assert.NotNil(b1) { - assert.Equal("backend1", b1.Id()) - assert.Equal(string(testBackendSecret), string(b1.Secret())) - assert.Equal([]string{u2.String()}, b1.Urls()) - } - - updated_config = goconf.NewConfigFile() - updated_config.AddOption("backend", "backends", "") - updated_config.AddOption("backend", "secret", string(testBackendSecret)) - cfg.Reload(updated_config) - - assert.Equal(0, stats.value) - b1 := cfg.GetBackend(u2) - assert.Nil(b1) -} - -func TestBackendConfiguration_EtcdChangeUrls(t *testing.T) { - t.Parallel() - stats := &mockBackendStats{} - - logger := logtest.NewLoggerForTest(t) - require := require.New(t) - assert := assert.New(t) - embedEtcd, client := etcdtest.NewClientForTest(t) - - url1 := "https://domain1.invalid/foo" - initialSecret1 := string(testBackendSecret) + "-backend1-initial" - secret1 := string(testBackendSecret) + "-backend1" - - embedEtcd.SetValue("/backends/1_one", []byte("{\"urls\":[\""+url1+"\"],\"secret\":\""+initialSecret1+"\"}")) - - config := goconf.NewConfigFile() - config.AddOption("backend", "backendtype", "etcd") - config.AddOption("backend", "backendprefix", "/backends") - - cfg, err := NewBackendConfigurationWithStats(logger, config, client, stats) - require.NoError(err) - defer cfg.Close() - - storage := cfg.storage.(*backendStorageEtcd) - ch := storage.getWakeupChannelForTesting() - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - require.NoError(storage.WaitForInitialized(ctx)) - - assert.Equal(1, stats.value) - if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 1) && - assert.Equal([]string{url1}, backends[0].Urls()) && - assert.Equal(initialSecret1, string(backends[0].Secret())) { - if backend := cfg.GetBackend(mustParse(url1)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) - } - } - - url2 := "https://domain1.invalid/bar" - - test.DrainWakeupChannel(ch) - embedEtcd.SetValue("/backends/1_one", []byte("{\"urls\":[\""+url1+"\",\""+url2+"\"],\"secret\":\""+secret1+"\"}")) - <-ch - assert.Equal(1, stats.value) - if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 1) && - assert.Equal([]string{url2, url1}, backends[0].Urls()) && - assert.Equal(secret1, string(backends[0].Secret())) { - if backend := cfg.GetBackend(mustParse(url1)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) - } - if backend := cfg.GetBackend(mustParse(url2)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) - } - } - - url3 := "https://domain2.invalid/foo" - secret3 := string(testBackendSecret) + "-backend3" - - url4 := "https://domain3.invalid/foo" - - test.DrainWakeupChannel(ch) - embedEtcd.SetValue("/backends/3_three", []byte("{\"urls\":[\""+url3+"\",\""+url4+"\"],\"secret\":\""+secret3+"\"}")) - <-ch - assert.Equal(2, stats.value) - if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 2) && - assert.Equal([]string{url2, url1}, backends[0].Urls()) && - assert.Equal(secret1, string(backends[0].Secret())) && - assert.Equal([]string{url3, url4}, backends[1].Urls()) && - assert.Equal(secret3, string(backends[1].Secret())) { - if backend := cfg.GetBackend(mustParse(url1)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) - } else if backend := cfg.GetBackend(mustParse(url2)); assert.NotNil(backend) { - assert.Equal(backends[0], backend) - } else if backend := cfg.GetBackend(mustParse(url3)); assert.NotNil(backend) { - assert.Equal(backends[1], backend) - } else if backend := cfg.GetBackend(mustParse(url4)); assert.NotNil(backend) { - assert.Equal(backends[1], backend) - } - } - - test.DrainWakeupChannel(ch) - embedEtcd.DeleteValue("/backends/1_one") - <-ch - assert.Equal(1, stats.value) - if backends := sortBackends(cfg.GetBackends()); assert.Len(backends, 1) { - assert.Equal([]string{url3, url4}, backends[0].Urls()) - assert.Equal(secret3, string(backends[0].Secret())) - } - - test.DrainWakeupChannel(ch) - embedEtcd.DeleteValue("/backends/3_three") - <-ch - - assert.Equal(0, stats.value) - storage.mu.RLock() - _, found := storage.backends["domain1.invalid"] - storage.mu.RUnlock() - assert.False(found, "Should have removed host information") -} diff --git a/server/backend_server.go b/backend_server.go similarity index 53% rename from server/backend_server.go rename to backend_server.go index 67c549a..016f0eb 100644 --- a/server/backend_server.go +++ b/backend_server.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" @@ -31,14 +31,12 @@ import ( "errors" "fmt" "io" + "log" "net" "net/http" - "net/http/pprof" "net/url" "reflect" "regexp" - runtimepprof "runtime/pprof" - "slices" "strings" "sync" "sync/atomic" @@ -47,16 +45,6 @@ import ( "github.com/dlintw/goconf" "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus/promhttp" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/pool" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) const ( @@ -64,19 +52,15 @@ const ( randomUsernameLength = 32 - sessionIdNotInMeeting = api.RoomSessionId("0") - - startDialoutTimeout = 45 * time.Second + sessionIdNotInMeeting = "0" ) type BackendServer struct { - logger log.Logger hub *Hub - events events.AsyncEvents + events AsyncEvents roomSessions RoomSessions version string - debug bool welcomeMessage string turnapikey string @@ -84,47 +68,51 @@ type BackendServer struct { turnvalid time.Duration turnservers []string - statsAllowedIps atomic.Pointer[container.IPList] + statsAllowedIps atomic.Pointer[AllowedIps] invalidSecret []byte - - buffers pool.BufferPool } -func NewBackendServer(ctx context.Context, cfg *goconf.ConfigFile, hub *Hub, version string) (*BackendServer, error) { - logger := log.LoggerFromContext(ctx) - turnapikey, _ := config.GetStringOptionWithEnv(cfg, "turn", "apikey") - turnsecret, _ := config.GetStringOptionWithEnv(cfg, "turn", "secret") - turnservers, _ := cfg.GetString("turn", "servers") +func NewBackendServer(config *goconf.ConfigFile, hub *Hub, version string) (*BackendServer, error) { + turnapikey, _ := config.GetString("turn", "apikey") + turnsecret, _ := config.GetString("turn", "secret") + turnservers, _ := config.GetString("turn", "servers") // TODO(jojo): Make the validity for TURN credentials configurable. turnvalid := 24 * time.Hour - turnserverslist := slices.Collect(internal.SplitEntries(turnservers, ",")) - if len(turnserverslist) != 0 { - if turnapikey == "" { - return nil, errors.New("need a TURN API key if TURN servers are configured") - } - if turnsecret == "" { - return nil, errors.New("need a shared TURN secret if TURN servers are configured") - } - - logger.Printf("Using configured TURN API key") - logger.Printf("Using configured shared TURN secret") - for _, s := range turnserverslist { - logger.Printf("Adding \"%s\" as TURN server", s) + var turnserverslist []string + for _, s := range strings.Split(turnservers, ",") { + s = strings.TrimSpace(s) + if s != "" { + turnserverslist = append(turnserverslist, s) } } - statsAllowed, _ := cfg.GetString("stats", "allowed_ips") - statsAllowedIps, err := container.ParseIPList(statsAllowed) + if len(turnserverslist) != 0 { + if turnapikey == "" { + return nil, fmt.Errorf("need a TURN API key if TURN servers are configured") + } + if turnsecret == "" { + return nil, fmt.Errorf("need a shared TURN secret if TURN servers are configured") + } + + log.Printf("Using configured TURN API key") + log.Printf("Using configured shared TURN secret") + for _, s := range turnserverslist { + log.Printf("Adding \"%s\" as TURN server", s) + } + } + + statsAllowed, _ := config.GetString("stats", "allowed_ips") + statsAllowedIps, err := ParseAllowedIps(statsAllowed) if err != nil { return nil, err } if !statsAllowedIps.Empty() { - logger.Printf("Only allowing access to the stats endpoint from %s", statsAllowed) + log.Printf("Only allowing access to the stats endpoint from %s", statsAllowed) } else { - statsAllowedIps = container.DefaultAllowedIPs() - logger.Printf("No IPs configured for the stats endpoint, only allowing access from %s", statsAllowedIps) + log.Printf("No IPs configured for the stats endpoint, only allowing access from 127.0.0.1") + statsAllowedIps = DefaultAllowedIps() } invalidSecret := make([]byte, 32) @@ -132,15 +120,11 @@ func NewBackendServer(ctx context.Context, cfg *goconf.ConfigFile, hub *Hub, ver return nil, err } - debug, _ := cfg.GetBool("app", "debug") - result := &BackendServer{ - logger: logger, hub: hub, events: hub.events, roomSessions: hub.roomSessions, version: version, - debug: debug, turnapikey: turnapikey, turnsecret: []byte(turnsecret), @@ -157,16 +141,16 @@ func NewBackendServer(ctx context.Context, cfg *goconf.ConfigFile, hub *Hub, ver func (b *BackendServer) Reload(config *goconf.ConfigFile) { statsAllowed, _ := config.GetString("stats", "allowed_ips") - if statsAllowedIps, err := container.ParseIPList(statsAllowed); err == nil { + if statsAllowedIps, err := ParseAllowedIps(statsAllowed); err == nil { if !statsAllowedIps.Empty() { - b.logger.Printf("Only allowing access to the stats endpoint from %s", statsAllowed) + log.Printf("Only allowing access to the stats endpoint from %s", statsAllowed) } else { - statsAllowedIps = container.DefaultAllowedIPs() - b.logger.Printf("No IPs configured for the stats endpoint, only allowing access from %s", statsAllowedIps) + log.Printf("No IPs configured for the stats endpoint, only allowing access from 127.0.0.1") + statsAllowedIps = DefaultAllowedIps() } b.statsAllowedIps.Store(statsAllowedIps) } else { - b.logger.Printf("Error parsing allowed stats ips from \"%s\": %s", statsAllowedIps, err) + log.Printf("Error parsing allowed stats ips from \"%s\": %s", statsAllowedIps, err) } } @@ -175,45 +159,29 @@ func (b *BackendServer) Start(r *mux.Router) error { "nextcloud-spreed-signaling": "Welcome", "version": b.version, } - welcomeMessage, _ := json.Marshal(welcome) - b.welcomeMessage = string(welcomeMessage) + "\n" - - if b.debug { - b.logger.Println("Installing debug handlers in \"/debug/pprof\"") - s := r.PathPrefix("/debug/pprof").Subrouter() - s.HandleFunc("", b.setCommonHeaders(b.validateStatsRequest(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/debug/pprof/", http.StatusTemporaryRedirect) - }))) - s.HandleFunc("/", b.setCommonHeaders(b.validateStatsRequest(pprof.Index))) - s.HandleFunc("/cmdline", b.setCommonHeaders(b.validateStatsRequest(pprof.Cmdline))) - s.HandleFunc("/profile", b.setCommonHeaders(b.validateStatsRequest(pprof.Profile))) - s.HandleFunc("/symbol", b.setCommonHeaders(b.validateStatsRequest(pprof.Symbol))) - s.HandleFunc("/trace", b.setCommonHeaders(b.validateStatsRequest(pprof.Trace))) - for _, profile := range runtimepprof.Profiles() { - name := profile.Name() - handler := pprof.Handler(name) - s.HandleFunc("/"+name, b.setCommonHeaders(b.validateStatsRequest(func(w http.ResponseWriter, r *http.Request) { - handler.ServeHTTP(w, r) - }))) - } + welcomeMessage, err := json.Marshal(welcome) + if err != nil { + // Should never happen. + return err } + b.welcomeMessage = string(welcomeMessage) + "\n" + s := r.PathPrefix("/api/v1").Subrouter() - s.HandleFunc("/welcome", b.setCommonHeaders(b.welcomeFunc)).Methods("GET") - s.HandleFunc("/room/{roomid}", b.setCommonHeaders(b.parseRequestBody(b.roomHandler))).Methods("POST") - s.HandleFunc("/stats", b.setCommonHeaders(b.validateStatsRequest(b.statsHandler))).Methods("GET") - s.HandleFunc("/serverinfo", b.setCommonHeaders(b.validateStatsRequest(b.serverinfoHandler))).Methods("GET") + s.HandleFunc("/welcome", b.setComonHeaders(b.welcomeFunc)).Methods("GET") + s.HandleFunc("/room/{roomid}", b.setComonHeaders(b.parseRequestBody(b.roomHandler))).Methods("POST") + s.HandleFunc("/stats", b.setComonHeaders(b.validateStatsRequest(b.statsHandler))).Methods("GET") // Expose prometheus metrics at "/metrics". - r.HandleFunc("/metrics", b.setCommonHeaders(b.validateStatsRequest(b.metricsHandler))).Methods("GET") + r.HandleFunc("/metrics", b.setComonHeaders(b.validateStatsRequest(b.metricsHandler))).Methods("GET") // Provide a REST service to get TURN credentials. // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 - r.HandleFunc("/turn/credentials", b.setCommonHeaders(b.getTurnCredentials)).Methods("GET") + r.HandleFunc("/turn/credentials", b.setComonHeaders(b.getTurnCredentials)).Methods("GET") return nil } -func (b *BackendServer) setCommonHeaders(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { +func (b *BackendServer) setComonHeaders(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "nextcloud-spreed-signaling/"+b.version) w.Header().Set("X-Spreed-Signaling-Features", strings.Join(b.hub.info.Features, ", ")) @@ -265,11 +233,11 @@ func (b *BackendServer) getTurnCredentials(w http.ResponseWriter, r *http.Reques if username == "" { // Make sure to include an actual username in the credentials. - username = internal.RandomString(randomUsernameLength) + username = newRandomString(randomUsernameLength) } username, password := calculateTurnSecret(username, b.turnsecret, b.turnvalid) - result := talk.TurnCredentials{ + result := TurnCredentials{ Username: username, Password: password, TTL: int64(b.turnvalid.Seconds()), @@ -278,7 +246,7 @@ func (b *BackendServer) getTurnCredentials(w http.ResponseWriter, r *http.Reques data, err := json.Marshal(result) if err != nil { - b.logger.Printf("Could not serialize TURN credentials: %s", err) + log.Printf("Could not serialize TURN credentials: %s", err) w.WriteHeader(http.StatusInternalServerError) io.WriteString(w, "Could not serialize credentials.") // nolint return @@ -293,7 +261,7 @@ func (b *BackendServer) getTurnCredentials(w http.ResponseWriter, r *http.Reques w.Write(data) // nolint } -func (b *BackendServer) parseRequestBody(f func(context.Context, http.ResponseWriter, *http.Request, []byte)) func(http.ResponseWriter, *http.Request) { +func (b *BackendServer) parseRequestBody(f func(http.ResponseWriter, *http.Request, []byte)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { // Sanity checks if r.ContentLength == -1 { @@ -305,39 +273,37 @@ func (b *BackendServer) parseRequestBody(f func(context.Context, http.ResponseWr } ct := r.Header.Get("Content-Type") if !strings.HasPrefix(ct, "application/json") { - b.logger.Printf("Received unsupported content-type: %s", ct) + log.Printf("Received unsupported content-type: %s", ct) http.Error(w, "Unsupported Content-Type", http.StatusBadRequest) return } - if r.Header.Get(talk.HeaderBackendSignalingRandom) == "" || - r.Header.Get(talk.HeaderBackendSignalingChecksum) == "" { + if r.Header.Get(HeaderBackendSignalingRandom) == "" || + r.Header.Get(HeaderBackendSignalingChecksum) == "" { http.Error(w, "Authentication check failed", http.StatusForbidden) return } - body, err := b.buffers.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) if err != nil { - b.logger.Println("Error reading body: ", err) + log.Println("Error reading body: ", err) http.Error(w, "Could not read body", http.StatusBadRequest) return } - defer b.buffers.Put(body) - ctx := log.NewLoggerContext(r.Context(), b.logger) - f(ctx, w, r, body.Bytes()) + f(w, r, body) } } -func (b *BackendServer) sendRoomInvite(roomid string, backend *talk.Backend, userids []string, properties json.RawMessage) { - msg := &events.AsyncMessage{ +func (b *BackendServer) sendRoomInvite(roomid string, backend *Backend, userids []string, properties json.RawMessage) { + msg := &AsyncMessage{ Type: "message", - Message: &api.ServerMessage{ + Message: &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "roomlist", Type: "invite", - Invite: &api.RoomEventServerMessage{ + Invite: &RoomEventServerMessage{ RoomId: roomid, Properties: properties, }, @@ -346,21 +312,21 @@ func (b *BackendServer) sendRoomInvite(roomid string, backend *talk.Backend, use } for _, userid := range userids { if err := b.events.PublishUserMessage(userid, backend, msg); err != nil { - b.logger.Printf("Could not publish room invite for user %s in backend %s: %s", userid, backend.Id(), err) + log.Printf("Could not publish room invite for user %s in backend %s: %s", userid, backend.Id(), err) } } } -func (b *BackendServer) sendRoomDisinvite(roomid string, backend *talk.Backend, reason string, userids []string, sessionids []api.RoomSessionId) { - msg := &events.AsyncMessage{ +func (b *BackendServer) sendRoomDisinvite(roomid string, backend *Backend, reason string, userids []string, sessionids []string) { + msg := &AsyncMessage{ Type: "message", - Message: &api.ServerMessage{ + Message: &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "roomlist", Type: "disinvite", - Disinvite: &api.RoomDisinviteEventServerMessage{ - RoomEventServerMessage: api.RoomEventServerMessage{ + Disinvite: &RoomDisinviteEventServerMessage{ + RoomEventServerMessage: RoomEventServerMessage{ RoomId: roomid, }, Reason: reason, @@ -370,13 +336,12 @@ func (b *BackendServer) sendRoomDisinvite(roomid string, backend *talk.Backend, } for _, userid := range userids { if err := b.events.PublishUserMessage(userid, backend, msg); err != nil { - b.logger.Printf("Could not publish room disinvite for user %s in backend %s: %s", userid, backend.Id(), err) + log.Printf("Could not publish room disinvite for user %s in backend %s: %s", userid, backend.Id(), err) } } timeout := time.Second - ctx := log.NewLoggerContext(context.Background(), b.logger) - ctx, cancel := context.WithTimeout(ctx, timeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() var wg sync.WaitGroup for _, sessionid := range sessionids { @@ -385,28 +350,30 @@ func (b *BackendServer) sendRoomDisinvite(roomid string, backend *talk.Backend, continue } - wg.Go(func() { + wg.Add(1) + go func(sessionid string) { + defer wg.Done() if sid, err := b.lookupByRoomSessionId(ctx, sessionid, nil); err != nil { - b.logger.Printf("Could not lookup by room session %s: %s", sessionid, err) + log.Printf("Could not lookup by room session %s: %s", sessionid, err) } else if sid != "" { if err := b.events.PublishSessionMessage(sid, backend, msg); err != nil { - b.logger.Printf("Could not publish room disinvite for session %s: %s", sid, err) + log.Printf("Could not publish room disinvite for session %s: %s", sid, err) } } - }) + }(sessionid) } wg.Wait() } -func (b *BackendServer) sendRoomUpdate(roomid string, backend *talk.Backend, notified_userids []string, all_userids []string, properties json.RawMessage) { - msg := &events.AsyncMessage{ +func (b *BackendServer) sendRoomUpdate(roomid string, backend *Backend, notified_userids []string, all_userids []string, properties json.RawMessage) { + msg := &AsyncMessage{ Type: "message", - Message: &api.ServerMessage{ + Message: &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "roomlist", Type: "update", - Update: &api.RoomEventServerMessage{ + Update: &RoomEventServerMessage{ RoomId: roomid, Properties: properties, }, @@ -424,14 +391,14 @@ func (b *BackendServer) sendRoomUpdate(roomid string, backend *talk.Backend, not } if err := b.events.PublishUserMessage(userid, backend, msg); err != nil { - b.logger.Printf("Could not publish room update for user %s in backend %s: %s", userid, backend.Id(), err) + log.Printf("Could not publish room update for user %s in backend %s: %s", userid, backend.Id(), err) } } } -func (b *BackendServer) lookupByRoomSessionId(ctx context.Context, roomSessionId api.RoomSessionId, cache *container.ConcurrentMap[api.RoomSessionId, api.PublicSessionId]) (api.PublicSessionId, error) { +func (b *BackendServer) lookupByRoomSessionId(ctx context.Context, roomSessionId string, cache *ConcurrentStringStringMap) (string, error) { if roomSessionId == sessionIdNotInMeeting { - b.logger.Printf("Trying to lookup empty room session id: %s", roomSessionId) + log.Printf("Trying to lookup empty room session id: %s", roomSessionId) return "", nil } @@ -454,41 +421,48 @@ func (b *BackendServer) lookupByRoomSessionId(ctx context.Context, roomSessionId return sid, nil } -func (b *BackendServer) fixupUserSessions(ctx context.Context, cache *container.ConcurrentMap[api.RoomSessionId, api.PublicSessionId], users []api.StringMap) []api.StringMap { +func (b *BackendServer) fixupUserSessions(ctx context.Context, cache *ConcurrentStringStringMap, users []map[string]interface{}) []map[string]interface{} { if len(users) == 0 { return users } var wg sync.WaitGroup for _, user := range users { - roomSessionId, found := api.GetStringMapString[api.RoomSessionId](user, "sessionId") + roomSessionIdOb, found := user["sessionId"] if !found { - b.logger.Printf("User %+v has invalid room session id, ignoring", user) + continue + } + + roomSessionId, ok := roomSessionIdOb.(string) + if !ok { + log.Printf("User %+v has invalid room session id, ignoring", user) delete(user, "sessionId") continue } if roomSessionId == sessionIdNotInMeeting { - b.logger.Printf("User %+v is not in the meeting, ignoring", user) + log.Printf("User %+v is not in the meeting, ignoring", user) delete(user, "sessionId") continue } - wg.Go(func() { + wg.Add(1) + go func(roomSessionId string, u map[string]interface{}) { + defer wg.Done() if sessionId, err := b.lookupByRoomSessionId(ctx, roomSessionId, cache); err != nil { - b.logger.Printf("Could not lookup by room session %s: %s", roomSessionId, err) - delete(user, "sessionId") + log.Printf("Could not lookup by room session %s: %s", roomSessionId, err) + delete(u, "sessionId") } else if sessionId != "" { - user["sessionId"] = sessionId + u["sessionId"] = sessionId } else { // sessionId == "" - delete(user, "sessionId") + delete(u, "sessionId") } - }) + }(roomSessionId, user) } wg.Wait() - result := make([]api.StringMap, 0, len(users)) + result := make([]map[string]interface{}, 0, len(users)) for _, user := range users { if _, found := user["sessionId"]; found { result = append(result, user) @@ -497,14 +471,13 @@ func (b *BackendServer) fixupUserSessions(ctx context.Context, cache *container. return result } -func (b *BackendServer) sendRoomIncall(roomid string, backend *talk.Backend, request *talk.BackendServerRoomRequest) error { +func (b *BackendServer) sendRoomIncall(roomid string, backend *Backend, request *BackendServerRoomRequest) error { if !request.InCall.All { timeout := time.Second - ctx := log.NewLoggerContext(context.Background(), b.logger) - ctx, cancel := context.WithTimeout(ctx, timeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - var cache container.ConcurrentMap[api.RoomSessionId, api.PublicSessionId] + var cache ConcurrentStringStringMap // Convert (Nextcloud) session ids to signaling session ids. request.InCall.Users = b.fixupUserSessions(ctx, &cache, request.InCall.Users) // Entries in "Changed" are most likely already fetched through the "Users" list. @@ -515,20 +488,20 @@ func (b *BackendServer) sendRoomIncall(roomid string, backend *talk.Backend, req } } - message := &events.AsyncMessage{ + message := &AsyncMessage{ Type: "room", Room: request, } return b.events.PublishBackendRoomMessage(roomid, backend, message) } -func (b *BackendServer) sendRoomParticipantsUpdate(ctx context.Context, roomid string, backend *talk.Backend, request *talk.BackendServerRoomRequest) error { +func (b *BackendServer) sendRoomParticipantsUpdate(roomid string, backend *Backend, request *BackendServerRoomRequest) error { timeout := time.Second // Convert (Nextcloud) session ids to signaling session ids. - ctx, cancel := context.WithTimeout(ctx, timeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - var cache container.ConcurrentMap[api.RoomSessionId, api.PublicSessionId] + var cache ConcurrentStringStringMap request.Participants.Users = b.fixupUserSessions(ctx, &cache, request.Participants.Users) request.Participants.Changed = b.fixupUserSessions(ctx, &cache, request.Participants.Changed) @@ -544,59 +517,56 @@ loop: continue } - sessionId, found := api.GetStringMapString[api.PublicSessionId](user, "sessionId") - if !found { - b.logger.Printf("User entry has no session id: %+v", user) - continue - } - - permissionsList, ok := permissionsInterface.([]any) + sessionId := user["sessionId"].(string) + permissionsList, ok := permissionsInterface.([]interface{}) if !ok { - b.logger.Printf("Received invalid permissions %+v (%s) for session %s", permissionsInterface, reflect.TypeOf(permissionsInterface), sessionId) + log.Printf("Received invalid permissions %+v (%s) for session %s", permissionsInterface, reflect.TypeOf(permissionsInterface), sessionId) continue } - var permissions []api.Permission + var permissions []Permission for idx, ob := range permissionsList { permission, ok := ob.(string) if !ok { - b.logger.Printf("Received invalid permission at position %d %+v (%s) for session %s", idx, ob, reflect.TypeOf(ob), sessionId) + log.Printf("Received invalid permission at position %d %+v (%s) for session %s", idx, ob, reflect.TypeOf(ob), sessionId) continue loop } - permissions = append(permissions, api.Permission(permission)) + permissions = append(permissions, Permission(permission)) } + wg.Add(1) - wg.Go(func() { - message := &events.AsyncMessage{ + go func(sessionId string, permissions []Permission) { + defer wg.Done() + message := &AsyncMessage{ Type: "permissions", Permissions: permissions, } if err := b.events.PublishSessionMessage(sessionId, backend, message); err != nil { - b.logger.Printf("Could not send permissions update (%+v) to session %s: %s", permissions, sessionId, err) + log.Printf("Could not send permissions update (%+v) to session %s: %s", permissions, sessionId, err) } - }) + }(sessionId, permissions) } wg.Wait() - message := &events.AsyncMessage{ + message := &AsyncMessage{ Type: "room", Room: request, } return b.events.PublishBackendRoomMessage(roomid, backend, message) } -func (b *BackendServer) sendRoomMessage(roomid string, backend *talk.Backend, request *talk.BackendServerRoomRequest) error { - message := &events.AsyncMessage{ +func (b *BackendServer) sendRoomMessage(roomid string, backend *Backend, request *BackendServerRoomRequest) error { + message := &AsyncMessage{ Type: "room", Room: request, } return b.events.PublishBackendRoomMessage(roomid, backend, message) } -func (b *BackendServer) sendRoomSwitchTo(ctx context.Context, roomid string, backend *talk.Backend, request *talk.BackendServerRoomRequest) error { +func (b *BackendServer) sendRoomSwitchTo(roomid string, backend *Backend, request *BackendServerRoomRequest) error { timeout := time.Second // Convert (Nextcloud) session ids to signaling session ids. - ctx, cancel := context.WithTimeout(ctx, timeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() var wg sync.WaitGroup @@ -604,7 +574,7 @@ func (b *BackendServer) sendRoomSwitchTo(ctx context.Context, roomid string, bac if len(request.SwitchTo.Sessions) > 0 { // We support both a list of sessions or a map with additional details per session. if request.SwitchTo.Sessions[0] == '[' { - var sessionsList talk.BackendRoomSwitchToSessionsList + var sessionsList BackendRoomSwitchToSessionsList if err := json.Unmarshal(request.SwitchTo.Sessions, &sessionsList); err != nil { return err } @@ -613,21 +583,23 @@ func (b *BackendServer) sendRoomSwitchTo(ctx context.Context, roomid string, bac return nil } - var internalSessionsList talk.BackendRoomSwitchToPublicSessionsList + var internalSessionsList BackendRoomSwitchToSessionsList for _, roomSessionId := range sessionsList { if roomSessionId == sessionIdNotInMeeting { continue } - wg.Go(func() { + wg.Add(1) + go func(roomSessionId string) { + defer wg.Done() if sessionId, err := b.lookupByRoomSessionId(ctx, roomSessionId, nil); err != nil { - b.logger.Printf("Could not lookup by room session %s: %s", roomSessionId, err) + log.Printf("Could not lookup by room session %s: %s", roomSessionId, err) } else if sessionId != "" { mu.Lock() defer mu.Unlock() internalSessionsList = append(internalSessionsList, sessionId) } - }) + }(roomSessionId) } wg.Wait() @@ -640,7 +612,7 @@ func (b *BackendServer) sendRoomSwitchTo(ctx context.Context, roomid string, bac request.SwitchTo.SessionsList = internalSessionsList request.SwitchTo.SessionsMap = nil } else { - var sessionsMap talk.BackendRoomSwitchToSessionsMap + var sessionsMap BackendRoomSwitchToSessionsMap if err := json.Unmarshal(request.SwitchTo.Sessions, &sessionsMap); err != nil { return err } @@ -649,21 +621,23 @@ func (b *BackendServer) sendRoomSwitchTo(ctx context.Context, roomid string, bac return nil } - internalSessionsMap := make(talk.BackendRoomSwitchToPublicSessionsMap) + internalSessionsMap := make(BackendRoomSwitchToSessionsMap) for roomSessionId, details := range sessionsMap { if roomSessionId == sessionIdNotInMeeting { continue } - wg.Go(func() { + wg.Add(1) + go func(roomSessionId string, details json.RawMessage) { + defer wg.Done() if sessionId, err := b.lookupByRoomSessionId(ctx, roomSessionId, nil); err != nil { - b.logger.Printf("Could not lookup by room session %s: %s", roomSessionId, err) + log.Printf("Could not lookup by room session %s: %s", roomSessionId, err) } else if sessionId != "" { mu.Lock() defer mu.Unlock() internalSessionsMap[sessionId] = details } - }) + }(roomSessionId, details) } wg.Wait() @@ -679,7 +653,7 @@ func (b *BackendServer) sendRoomSwitchTo(ctx context.Context, roomid string, bac } request.SwitchTo.Sessions = nil - message := &events.AsyncMessage{ + message := &AsyncMessage{ Type: "room", Room: request, } @@ -691,7 +665,7 @@ type BackendResponseWithStatus interface { } type DialoutErrorResponse struct { - talk.BackendServerRoomResponse + BackendServerRoomResponse status int } @@ -700,11 +674,11 @@ func (r *DialoutErrorResponse) Status() int { return r.status } -func returnDialoutError(status int, err *api.Error) (any, error) { +func returnDialoutError(status int, err *Error) (any, error) { response := &DialoutErrorResponse{ - BackendServerRoomResponse: talk.BackendServerRoomResponse{ + BackendServerRoomResponse: BackendServerRoomResponse{ Type: "dialout", - Dialout: &talk.BackendRoomDialoutResponse{ + Dialout: &BackendRoomDialoutResponse{ Error: err, }, }, @@ -720,42 +694,48 @@ func isNumeric(s string) bool { return checkNumeric.MatchString(s) } -func (b *BackendServer) startDialoutInSession(ctx context.Context, session *ClientSession, roomid string, backend *talk.Backend, backendUrl string, request *talk.BackendServerRoomRequest) (any, error) { - url := backendUrl - if url != "" && url[len(url)-1] != '/' { - url += "/" +func (b *BackendServer) startDialout(roomid string, backend *Backend, backendUrl string, request *BackendServerRoomRequest) (any, error) { + if err := request.Dialout.ValidateNumber(); err != nil { + return returnDialoutError(http.StatusBadRequest, err) } - if urls := backend.Urls(); len(urls) > 0 { - // Check if client-provided URL is registered for backend and use that. - if !slices.ContainsFunc(urls, func(u string) bool { - return strings.HasPrefix(url, u) - }) { - url = urls[0] + + if !isNumeric(roomid) { + return returnDialoutError(http.StatusBadRequest, NewError("invalid_roomid", "The room id must be numeric.")) + } + + session := b.hub.GetDialoutSession(roomid, backend) + if session == nil { + return returnDialoutError(http.StatusNotFound, NewError("no_client_available", "No available client found to trigger dialout.")) + } + + url := backend.Url() + if url == "" { + // Old-style compat backend, use client-provided URL. + url = backendUrl + if url != "" && url[len(url)-1] != '/' { + url += "/" } } - id := internal.RandomString(32) - msg := &api.ServerMessage{ + id := newRandomString(32) + msg := &ServerMessage{ Id: id, Type: "internal", - Internal: &api.InternalServerMessage{ + Internal: &InternalServerMessage{ Type: "dialout", - Dialout: &api.InternalServerDialoutRequest{ + Dialout: &InternalServerDialoutRequest{ RoomId: roomid, Backend: url, - Request: &api.InternalServerDialoutRequestContents{ - Number: request.Dialout.Number, - Options: request.Dialout.Options, - }, + Request: request.Dialout, }, }, } - subCtx, cancel := context.WithTimeout(ctx, startDialoutTimeout) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - var response atomic.Pointer[api.DialoutInternalClientMessage] + var response atomic.Pointer[DialoutInternalClientMessage] - session.HandleResponse(id, func(message *api.ClientMessage) bool { + session.HandleResponse(id, func(message *ClientMessage) bool { response.Store(message.Internal.Dialout) cancel() // Don't send error to other sessions in the room. @@ -764,88 +744,47 @@ func (b *BackendServer) startDialoutInSession(ctx context.Context, session *Clie defer session.ClearResponseHandler(id) if !session.SendMessage(msg) { - return nil, api.NewError("error_notify", "Could not notify about new dialout.") + return returnDialoutError(http.StatusBadGateway, NewError("error_notify", "Could not notify about new dialout.")) } - <-subCtx.Done() - if err := subCtx.Err(); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return nil, api.NewError("timeout", "Timeout while waiting for dialout to start.") - } else if errors.Is(err, context.Canceled) && errors.Is(ctx.Err(), context.Canceled) { - // Upstream request was cancelled. - return nil, err - } + <-ctx.Done() + if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) { + return returnDialoutError(http.StatusGatewayTimeout, NewError("timeout", "Timeout while waiting for dialout to start.")) } dialout := response.Load() if dialout == nil { - return nil, api.NewError("error_notify", "No dialout response received.") + return returnDialoutError(http.StatusBadGateway, NewError("error_notify", "No dialout response received.")) } switch dialout.Type { case "error": - return nil, dialout.Error + return returnDialoutError(http.StatusBadGateway, dialout.Error) case "status": - if dialout.Status.Status != api.DialoutStatusAccepted { - return nil, api.NewError("unsupported_status", fmt.Sprintf("Unsupported dialout status received: %+v", dialout)) + if dialout.Status.Status != DialoutStatusAccepted { + log.Printf("Received unsupported dialout status when triggering dialout: %+v", dialout) + return returnDialoutError(http.StatusBadGateway, NewError("unsupported_status", "Unsupported dialout status received.")) } - return &talk.BackendServerRoomResponse{ + return &BackendServerRoomResponse{ Type: "dialout", - Dialout: &talk.BackendRoomDialoutResponse{ + Dialout: &BackendRoomDialoutResponse{ CallId: dialout.Status.CallId, }, }, nil - default: - return nil, api.NewError("unsupported_type", fmt.Sprintf("Unsupported dialout type received: %+v", dialout)) } + + log.Printf("Received unsupported dialout type when triggering dialout: %+v", dialout) + return returnDialoutError(http.StatusBadGateway, NewError("unsupported_type", "Unsupported dialout type received.")) } -func (b *BackendServer) startDialout(ctx context.Context, roomid string, backend *talk.Backend, backendUrl string, request *talk.BackendServerRoomRequest) (any, error) { - if err := request.Dialout.ValidateNumber(); err != nil { - return returnDialoutError(http.StatusBadRequest, err) - } - - if !isNumeric(roomid) { - return returnDialoutError(http.StatusBadRequest, api.NewError("invalid_roomid", "The room id must be numeric.")) - } - - var sessionError *api.Error - sessions := b.hub.GetDialoutSessions(roomid, backend) - for _, session := range sessions { - if ctx.Err() != nil { - // Upstream request was cancelled. - break - } - - response, err := b.startDialoutInSession(ctx, session, roomid, backend, backendUrl, request) - if err != nil { - b.logger.Printf("Error starting dialout request %+v in session %s: %+v", request.Dialout, session.PublicId(), err) - if sessionError == nil { - if e, ok := internal.AsErrorType[*api.Error](err); ok { - sessionError = e - } - } - continue - } - - return response, nil - } - - if sessionError != nil { - return returnDialoutError(http.StatusBadGateway, sessionError) - } - - return returnDialoutError(http.StatusNotFound, api.NewError("no_client_available", "No available client found to trigger dialout.")) -} - -func (b *BackendServer) roomHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, body []byte) { - throttle, err := b.hub.throttler.CheckBruteforce(ctx, b.hub.getRealUserIP(r), "BackendRoomAuth") - if err == async.ErrBruteforceDetected { +func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body []byte) { + throttle, err := b.hub.throttler.CheckBruteforce(r.Context(), b.hub.getRealUserIP(r), "BackendRoomAuth") + if err == ErrBruteforceDetected { http.Error(w, "Too many requests", http.StatusTooManyRequests) return } else if err != nil { - b.logger.Printf("Error checking for bruteforce: %s", err) + log.Printf("Error checking for bruteforce: %s", err) http.Error(w, "Could not check for bruteforce", http.StatusInternalServerError) return } @@ -853,8 +792,8 @@ func (b *BackendServer) roomHandler(ctx context.Context, w http.ResponseWriter, v := mux.Vars(r) roomid := v["roomid"] - var backend *talk.Backend - backendUrl := r.Header.Get(talk.HeaderBackendServer) + var backend *Backend + backendUrl := r.Header.Get(HeaderBackendServer) if backendUrl != "" { if u, err := url.Parse(backendUrl); err == nil { backend = b.hub.backend.GetBackend(u) @@ -862,7 +801,7 @@ func (b *BackendServer) roomHandler(ctx context.Context, w http.ResponseWriter, if backend == nil { // Unknown backend URL passed, return immediately. - throttle(ctx) + throttle(r.Context()) http.Error(w, "Authentication check failed", http.StatusForbidden) return } @@ -876,7 +815,7 @@ func (b *BackendServer) roomHandler(ctx context.Context, w http.ResponseWriter, // Old-style Talk, find backend that created the checksum. // TODO(fancycode): Remove once all supported Talk versions send the backend header. for _, b := range b.hub.backend.GetBackends() { - if talk.ValidateBackendChecksum(r, body, b.Secret()) { + if ValidateBackendChecksum(r, body, b.Secret()) { backend = b break } @@ -884,21 +823,21 @@ func (b *BackendServer) roomHandler(ctx context.Context, w http.ResponseWriter, } if backend == nil { - throttle(ctx) + throttle(r.Context()) http.Error(w, "Authentication check failed", http.StatusForbidden) return } } - if !talk.ValidateBackendChecksum(r, body, backend.Secret()) { - throttle(ctx) + if !ValidateBackendChecksum(r, body, backend.Secret()) { + throttle(r.Context()) http.Error(w, "Authentication check failed", http.StatusForbidden) return } - var request talk.BackendServerRoomRequest + var request BackendServerRoomRequest if err := json.Unmarshal(body, &request); err != nil { - b.logger.Printf("Error decoding body %s: %s", string(body), err) + log.Printf("Error decoding body %s: %s", string(body), err) http.Error(w, "Could not read body", http.StatusBadRequest) return } @@ -911,39 +850,39 @@ func (b *BackendServer) roomHandler(ctx context.Context, w http.ResponseWriter, b.sendRoomInvite(roomid, backend, request.Invite.UserIds, request.Invite.Properties) b.sendRoomUpdate(roomid, backend, request.Invite.UserIds, request.Invite.AllUserIds, request.Invite.Properties) case "disinvite": - b.sendRoomDisinvite(roomid, backend, api.DisinviteReasonDisinvited, request.Disinvite.UserIds, request.Disinvite.SessionIds) + b.sendRoomDisinvite(roomid, backend, DisinviteReasonDisinvited, request.Disinvite.UserIds, request.Disinvite.SessionIds) b.sendRoomUpdate(roomid, backend, request.Disinvite.UserIds, request.Disinvite.AllUserIds, request.Disinvite.Properties) case "update": - message := &events.AsyncMessage{ + message := &AsyncMessage{ Type: "room", Room: &request, } err = b.events.PublishBackendRoomMessage(roomid, backend, message) b.sendRoomUpdate(roomid, backend, nil, request.Update.UserIds, request.Update.Properties) case "delete": - message := &events.AsyncMessage{ + message := &AsyncMessage{ Type: "room", Room: &request, } err = b.events.PublishBackendRoomMessage(roomid, backend, message) - b.sendRoomDisinvite(roomid, backend, api.DisinviteReasonDeleted, request.Delete.UserIds, nil) + b.sendRoomDisinvite(roomid, backend, DisinviteReasonDeleted, request.Delete.UserIds, nil) case "incall": err = b.sendRoomIncall(roomid, backend, &request) case "participants": - err = b.sendRoomParticipantsUpdate(ctx, roomid, backend, &request) + err = b.sendRoomParticipantsUpdate(roomid, backend, &request) case "message": err = b.sendRoomMessage(roomid, backend, &request) case "switchto": - err = b.sendRoomSwitchTo(ctx, roomid, backend, &request) + err = b.sendRoomSwitchTo(roomid, backend, &request) case "dialout": - response, err = b.startDialout(ctx, roomid, backend, backendUrl, &request) + response, err = b.startDialout(roomid, backend, backendUrl, &request) default: http.Error(w, "Unsupported request type: "+request.Type, http.StatusBadRequest) return } if err != nil { - b.logger.Printf("Error processing %s for room %s: %s", string(body), roomid, err) + log.Printf("Error processing %s for room %s: %s", string(body), roomid, err) http.Error(w, "Error while processing", http.StatusInternalServerError) return } @@ -959,7 +898,7 @@ func (b *BackendServer) roomHandler(ctx context.Context, w http.ResponseWriter, } responseData, err = json.Marshal(response) if err != nil { - b.logger.Printf("Could not serialize backend response %+v: %s", response, err) + log.Printf("Could not serialize backend response %+v: %s", response, err) responseStatus = http.StatusInternalServerError responseData = []byte("{\"error\":\"could_not_serialize\"}") } @@ -979,7 +918,7 @@ func (b *BackendServer) allowStatsAccess(r *http.Request) bool { } allowed := b.statsAllowedIps.Load() - return allowed != nil && allowed.Contains(ip) + return allowed != nil && allowed.Allowed(ip) } func (b *BackendServer) validateStatsRequest(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { @@ -997,7 +936,7 @@ func (b *BackendServer) statsHandler(w http.ResponseWriter, r *http.Request) { stats := b.hub.GetStats() statsData, err := json.MarshalIndent(stats, "", " ") if err != nil { - b.logger.Printf("Could not serialize stats %+v: %s", stats, err) + log.Printf("Could not serialize stats %+v: %s", stats, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } @@ -1008,43 +947,6 @@ func (b *BackendServer) statsHandler(w http.ResponseWriter, r *http.Request) { w.Write(statsData) // nolint } -type withServerInfoNats interface { - GetServerInfoNats() *talk.BackendServerInfoNats -} - -func (b *BackendServer) serverinfoHandler(w http.ResponseWriter, r *http.Request) { - info := talk.BackendServerInfo{ - Version: b.version, - Features: b.hub.info.Features, - - Dialout: b.hub.GetServerInfoDialout(), - } - if mcu := b.hub.mcu; mcu != nil { - info.Sfu = mcu.GetServerInfoSfu() - } - if e, ok := b.events.(withServerInfoNats); ok { - info.Nats = e.GetServerInfoNats() - } - if rpcClients := b.hub.rpcClients; rpcClients != nil { - info.Grpc = rpcClients.GetServerInfoGrpc() - } - if etcdClient := b.hub.etcdClient; etcdClient != nil { - info.Etcd = etcdClient.GetServerInfoEtcd() - } - - infoData, err := json.MarshalIndent(info, "", " ") - if err != nil { - b.logger.Printf("Could not serialize server info %+v: %s", info, err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(http.StatusOK) - w.Write(infoData) // nolint -} - func (b *BackendServer) metricsHandler(w http.ResponseWriter, r *http.Request) { promhttp.Handler().ServeHTTP(w, r) } diff --git a/server/backend_server_test.go b/backend_server_test.go similarity index 63% rename from server/backend_server_test.go rename to backend_server_test.go index 80338bd..858b7f3 100644 --- a/server/backend_server_test.go +++ b/backend_server_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "bytes" @@ -37,25 +37,14 @@ import ( "net/url" "strings" "sync" - "sync/atomic" "testing" "time" "github.com/dlintw/goconf" "github.com/gorilla/mux" + "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - eventstest "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events/test" - grpctest "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - natstest "github.com/strukturag/nextcloud-spreed-signaling/v2/nats/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) var ( @@ -65,11 +54,11 @@ var ( turnServers = strings.Split(turnServersString, ",") ) -func CreateBackendServerForTest(t *testing.T) (*goconf.ConfigFile, *BackendServer, events.AsyncEvents, *Hub, *mux.Router, *httptest.Server) { +func CreateBackendServerForTest(t *testing.T) (*goconf.ConfigFile, *BackendServer, AsyncEvents, *Hub, *mux.Router, *httptest.Server) { return CreateBackendServerForTestFromConfig(t, nil) } -func CreateBackendServerForTestWithTurn(t *testing.T) (*goconf.ConfigFile, *BackendServer, events.AsyncEvents, *Hub, *mux.Router, *httptest.Server) { +func CreateBackendServerForTestWithTurn(t *testing.T) (*goconf.ConfigFile, *BackendServer, AsyncEvents, *Hub, *mux.Router, *httptest.Server) { config := goconf.NewConfigFile() config.AddOption("turn", "apikey", turnApiKey) config.AddOption("turn", "secret", turnSecret) @@ -77,7 +66,7 @@ func CreateBackendServerForTestWithTurn(t *testing.T) (*goconf.ConfigFile, *Back return CreateBackendServerForTestFromConfig(t, config) } -func CreateBackendServerForTestFromConfig(t *testing.T, config *goconf.ConfigFile) (*goconf.ConfigFile, *BackendServer, events.AsyncEvents, *Hub, *mux.Router, *httptest.Server) { +func CreateBackendServerForTestFromConfig(t *testing.T, config *goconf.ConfigFile) (*goconf.ConfigFile, *BackendServer, AsyncEvents, *Hub, *mux.Router, *httptest.Server) { require := require.New(t) r := mux.NewRouter() registerBackendHandler(t, r) @@ -107,12 +96,10 @@ func CreateBackendServerForTestFromConfig(t *testing.T, config *goconf.ConfigFil config.AddOption("sessions", "blockkey", "09876543210987654321098765432109") config.AddOption("clients", "internalsecret", string(testInternalSecret)) config.AddOption("geoip", "url", "none") - events := eventstest.GetAsyncEventsForTest(t) - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - hub, err := NewHub(ctx, config, events, nil, nil, nil, r, "no-version") + events := getAsyncEventsForTest(t) + hub, err := NewHub(config, events, nil, nil, nil, r, "no-version") require.NoError(err) - b, err := NewBackendServer(ctx, config, hub, "no-version") + b, err := NewBackendServer(config, hub, "no-version") require.NoError(err) require.NoError(b.Start(r)) @@ -134,7 +121,6 @@ func CreateBackendServerWithClusteringForTest(t *testing.T) (*BackendServer, *Ba func CreateBackendServerWithClusteringForTestFromConfig(t *testing.T, config1 *goconf.ConfigFile, config2 *goconf.ConfigFile) (*BackendServer, *BackendServer, *Hub, *Hub, *httptest.Server, *httptest.Server) { require := require.New(t) - assert := assert.New(t) r1 := mux.NewRouter() registerBackendHandler(t, r1) @@ -151,9 +137,9 @@ func CreateBackendServerWithClusteringForTestFromConfig(t *testing.T, config1 *g server2.Close() }) - nats, _ := natstest.StartLocalServer(t) - grpcServer1, addr1 := grpctest.NewServerForTest(t) - grpcServer2, addr2 := grpctest.NewServerForTest(t) + nats := startLocalNatsServer(t) + grpcServer1, addr1 := NewGrpcServerForTest(t) + grpcServer2, addr2 := NewGrpcServerForTest(t) if config1 == nil { config1 = goconf.NewConfigFile() @@ -170,18 +156,13 @@ func CreateBackendServerWithClusteringForTestFromConfig(t *testing.T, config1 *g config1.AddOption("clients", "internalsecret", string(testInternalSecret)) config1.AddOption("geoip", "url", "none") - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - events1, err := events.NewAsyncEvents(ctx, nats.ClientURL()) + events1, err := NewAsyncEvents(nats) require.NoError(err) t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - assert.NoError(events1.Close(ctx)) + events1.Close() }) - client1, _ := grpctest.NewClientsForTest(t, addr2, nil) - hub1, err := NewHub(ctx, config1, events1, grpcServer1, client1, nil, r1, "no-version") + client1, _ := NewGrpcClientsForTest(t, addr2) + hub1, err := NewHub(config1, events1, grpcServer1, client1, nil, r1, "no-version") require.NoError(err) if config2 == nil { @@ -198,21 +179,19 @@ func CreateBackendServerWithClusteringForTestFromConfig(t *testing.T, config1 *g config2.AddOption("sessions", "blockkey", "09876543210987654321098765432109") config2.AddOption("clients", "internalsecret", string(testInternalSecret)) config2.AddOption("geoip", "url", "none") - events2, err := events.NewAsyncEvents(ctx, nats.ClientURL()) + events2, err := NewAsyncEvents(nats) require.NoError(err) t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - assert.NoError(events2.Close(ctx)) + events2.Close() }) - client2, _ := grpctest.NewClientsForTest(t, addr1, nil) - hub2, err := NewHub(ctx, config2, events2, grpcServer2, client2, nil, r2, "no-version") + client2, _ := NewGrpcClientsForTest(t, addr1) + hub2, err := NewHub(config2, events2, grpcServer2, client2, nil, r2, "no-version") require.NoError(err) - b1, err := NewBackendServer(ctx, config1, hub1, "no-version") + b1, err := NewBackendServer(config1, hub1, "no-version") require.NoError(err) require.NoError(b1.Start(r1)) - b2, err := NewBackendServer(ctx, config2, hub2, "no-version") + b2, err := NewBackendServer(config2, hub2, "no-version") require.NoError(err) require.NoError(b2.Start(r2)) @@ -236,8 +215,8 @@ func performBackendRequest(requestUrl string, body []byte) (*http.Response, erro return nil, err } request.Header.Set("Content-Type", "application/json") - rnd := internal.RandomString(32) - check := talk.CalculateBackendChecksum(rnd, body, testBackendSecret) + rnd := newRandomString(32) + check := CalculateBackendChecksum(rnd, body, testBackendSecret) request.Header.Set("Spreed-Signaling-Random", rnd) request.Header.Set("Spreed-Signaling-Checksum", check) u, err := url.Parse(requestUrl) @@ -249,36 +228,31 @@ func performBackendRequest(requestUrl string, body []byte) (*http.Response, erro return client.Do(request) } -func expectRoomlistEvent(t *testing.T, ch events.AsyncChannel, msgType string) (*api.EventServerMessage, bool) { - assert := assert.New(t) +func expectRoomlistEvent(ch chan *AsyncMessage, msgType string) (*EventServerMessage, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() select { - case natsMsg := <-ch: - var message events.AsyncMessage - if !assert.NoError(nats.Decode(natsMsg, &message)) || - !assert.Equal("message", message.Type, "invalid message type, got %+v", message) || - !assert.NotNil(message.Message, "message missing, got %+v", message) { - return nil, false + case message := <-ch: + if message.Type != "message" || message.Message == nil { + return nil, fmt.Errorf("Expected message type message, got %+v", message) } msg := message.Message - if !assert.Equal("event", msg.Type, "invalid message type, got %+v", msg) || - !assert.NotNil(msg.Event, "event missing, got %+v", msg) || - !assert.Equal("roomlist", msg.Event.Target, "invalid event target, got %+v", msg.Event) || - !assert.Equal(msgType, msg.Event.Type, "invalid event type, got %+v", msg.Event) { - return nil, false + if msg.Type != "event" || msg.Event == nil { + return nil, fmt.Errorf("Expected message type event, got %+v", msg) } - - return msg.Event, true + if msg.Event.Target != "roomlist" || msg.Event.Type != msgType { + return nil, fmt.Errorf("Expected roomlist %s event, got %+v", msgType, msg.Event) + } + return msg.Event, nil case <-ctx.Done(): - assert.NoError(ctx.Err()) - return nil, false + return nil, ctx.Err() } } func TestBackendServer_NoAuth(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, _, _, server := CreateBackendServerForTest(t) @@ -300,6 +274,7 @@ func TestBackendServer_NoAuth(t *testing.T) { func TestBackendServer_InvalidAuth(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, _, _, server := CreateBackendServerForTest(t) @@ -323,6 +298,7 @@ func TestBackendServer_InvalidAuth(t *testing.T) { func TestBackendServer_OldCompatAuth(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, _, _, server := CreateBackendServerForTest(t) @@ -330,9 +306,9 @@ func TestBackendServer_OldCompatAuth(t *testing.T) { roomId := "the-room-id" userid := "the-user-id" roomProperties := json.RawMessage("{\"foo\":\"bar\"}") - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "invite", - Invite: &talk.BackendRoomInviteRequest{ + Invite: &BackendRoomInviteRequest{ UserIds: []string{ userid, }, @@ -349,8 +325,8 @@ func TestBackendServer_OldCompatAuth(t *testing.T) { request, err := http.NewRequest("POST", server.URL+"/api/v1/room/"+roomId, bytes.NewReader(data)) require.NoError(err) request.Header.Set("Content-Type", "application/json") - rnd := internal.RandomString(32) - check := talk.CalculateBackendChecksum(rnd, data, testBackendSecret) + rnd := newRandomString(32) + check := CalculateBackendChecksum(rnd, data, testBackendSecret) request.Header.Set("Spreed-Signaling-Random", rnd) request.Header.Set("Spreed-Signaling-Checksum", check) client := &http.Client{} @@ -365,6 +341,7 @@ func TestBackendServer_OldCompatAuth(t *testing.T) { func TestBackendServer_InvalidBody(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, _, _, server := CreateBackendServerForTest(t) @@ -381,11 +358,12 @@ func TestBackendServer_InvalidBody(t *testing.T) { func TestBackendServer_UnsupportedRequest(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, _, _, server := CreateBackendServerForTest(t) - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "lala", } @@ -401,29 +379,27 @@ func TestBackendServer_UnsupportedRequest(t *testing.T) { } func TestBackendServer_RoomInvite(t *testing.T) { - t.Parallel() - for _, backend := range eventstest.EventBackendsForTest { + CatchLogForTest(t) + for _, backend := range eventBackendsForTest { t.Run(backend, func(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - RunTestBackendServer_RoomInvite(ctx, t) + RunTestBackendServer_RoomInvite(t) }) } } type channelEventListener struct { - ch events.AsyncChannel + ch chan *AsyncMessage } -func (l *channelEventListener) AsyncChannel() events.AsyncChannel { - return l.ch +func (l *channelEventListener) ProcessAsyncUserMessage(message *AsyncMessage) { + l.ch <- message } -func RunTestBackendServer_RoomInvite(ctx context.Context, t *testing.T) { +func RunTestBackendServer_RoomInvite(t *testing.T) { require := require.New(t) assert := assert.New(t) - _, _, asyncEvents, hub, _, server := CreateBackendServerForTest(t) + _, _, events, hub, _, server := CreateBackendServerForTest(t) u, err := url.Parse(server.URL) require.NoError(err) @@ -432,18 +408,16 @@ func RunTestBackendServer_RoomInvite(ctx context.Context, t *testing.T) { roomProperties := json.RawMessage("{\"foo\":\"bar\"}") backend := hub.backend.GetBackend(u) - eventsChan := make(events.AsyncChannel, 1) + eventsChan := make(chan *AsyncMessage, 1) listener := &channelEventListener{ ch: eventsChan, } - require.NoError(asyncEvents.RegisterUserListener(userid, backend, listener)) - defer func() { - assert.NoError(asyncEvents.UnregisterUserListener(userid, backend, listener)) - }() + require.NoError(events.RegisterUserListener(userid, backend, listener)) + defer events.UnregisterUserListener(userid, backend, listener) - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "invite", - Invite: &talk.BackendRoomInviteRequest{ + Invite: &BackendRoomInviteRequest{ UserIds: []string{ userid, }, @@ -464,68 +438,70 @@ func RunTestBackendServer_RoomInvite(ctx context.Context, t *testing.T) { assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s: %s", res.Status, string(body)) - if event, ok := expectRoomlistEvent(t, eventsChan, "invite"); ok && assert.NotNil(event.Invite) { - assert.Equal(roomId, event.Invite.RoomId) - assert.Equal(string(roomProperties), string(event.Invite.Properties)) + if event, err := expectRoomlistEvent(eventsChan, "invite"); assert.NoError(err) { + if assert.NotNil(event.Invite) { + assert.Equal(roomId, event.Invite.RoomId) + assert.Equal(string(roomProperties), string(event.Invite.Properties)) + } } } func TestBackendServer_RoomDisinvite(t *testing.T) { - t.Parallel() - for _, backend := range eventstest.EventBackendsForTest { + CatchLogForTest(t) + for _, backend := range eventBackendsForTest { t.Run(backend, func(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - RunTestBackendServer_RoomDisinvite(ctx, t) + RunTestBackendServer_RoomDisinvite(t) }) } } -func RunTestBackendServer_RoomDisinvite(ctx context.Context, t *testing.T) { +func RunTestBackendServer_RoomDisinvite(t *testing.T) { require := require.New(t) assert := assert.New(t) - _, _, asyncEvents, hub, _, server := CreateBackendServerForTest(t) + _, _, events, hub, _, server := CreateBackendServerForTest(t) u, err := url.Parse(server.URL) require.NoError(err) backend := hub.backend.GetBackend(u) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + require.NoError(client.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - defer client.CloseWithBye() + hello, err := client.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - if room, ok := client.JoinRoom(ctx, roomId); assert.True(ok) { + if room, err := client.JoinRoom(ctx, roomId); assert.NoError(err) { assert.Equal(roomId, room.Room.RoomId) } // Ignore "join" events. - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client.DrainMessages(ctx)) roomProperties := json.RawMessage("{\"foo\":\"bar\"}") - eventsChan := make(events.AsyncChannel, 1) + eventsChan := make(chan *AsyncMessage, 1) listener := &channelEventListener{ ch: eventsChan, } - require.NoError(asyncEvents.RegisterUserListener(testDefaultUserId, backend, listener)) - defer func() { - assert.NoError(asyncEvents.UnregisterUserListener(testDefaultUserId, backend, listener)) - }() + require.NoError(events.RegisterUserListener(testDefaultUserId, backend, listener)) + defer events.UnregisterUserListener(testDefaultUserId, backend, listener) - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "disinvite", - Disinvite: &talk.BackendRoomDisinviteRequest{ + Disinvite: &BackendRoomDisinviteRequest{ UserIds: []string{ testDefaultUserId, }, - SessionIds: []api.RoomSessionId{ - api.RoomSessionId(fmt.Sprintf("%s-%s"+roomId, hello.Hello.SessionId)), + SessionIds: []string{ + roomId + "-" + hello.Hello.SessionId, }, AllUserIds: []string{}, Properties: roomProperties, @@ -541,51 +517,65 @@ func RunTestBackendServer_RoomDisinvite(ctx context.Context, t *testing.T) { require.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s: %s", res.Status, string(body)) - if event, ok := expectRoomlistEvent(t, eventsChan, "disinvite"); ok && assert.NotNil(event.Disinvite) { - assert.Equal(roomId, event.Disinvite.RoomId) - assert.Equal("disinvited", event.Disinvite.Reason) + if event, err := expectRoomlistEvent(eventsChan, "disinvite"); assert.NoError(err) { + if assert.NotNil(event.Disinvite) { + assert.Equal(roomId, event.Disinvite.RoomId) + assert.Equal("disinvited", event.Disinvite.Reason) + } assert.Empty(string(event.Disinvite.Properties)) } - if message, ok := client.RunUntilRoomlistDisinvite(ctx); ok { + if message, err := client.RunUntilRoomlistDisinvite(ctx); assert.NoError(err) { assert.Equal(roomId, message.RoomId) } - client.RunUntilClosed(ctx) + if message, err := client.RunUntilMessage(ctx); err != nil && !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) { + assert.Fail("Received unexpected error %s", err) + } else if err == nil { + assert.Fail("Server should have closed the connection, received %+v", *message) + } } func TestBackendServer_RoomDisinviteDifferentRooms(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId)) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - defer client1.CloseWithBye() - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - defer client2.CloseWithBye() + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId1 := "test-room1" - MustSucceed2(t, client1.JoinRoom, ctx, roomId1) - require.True(client1.RunUntilJoined(ctx, hello1.Hello)) + _, err = client1.JoinRoom(ctx, roomId1) + require.NoError(err) + require.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) roomId2 := "test-room2" - MustSucceed2(t, client2.JoinRoom, ctx, roomId2) - require.True(client2.RunUntilJoined(ctx, hello2.Hello)) + _, err = client2.JoinRoom(ctx, roomId2) + require.NoError(err) + require.NoError(client2.RunUntilJoined(ctx, hello2.Hello)) - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "disinvite", - Disinvite: &talk.BackendRoomDisinviteRequest{ + Disinvite: &BackendRoomDisinviteRequest{ UserIds: []string{ testDefaultUserId, }, - SessionIds: []api.RoomSessionId{ - api.RoomSessionId(fmt.Sprintf("%s-%s"+roomId1, hello1.Hello.SessionId)), + SessionIds: []string{ + roomId1 + "-" + hello1.Hello.SessionId, }, AllUserIds: []string{}, }, @@ -600,19 +590,23 @@ func TestBackendServer_RoomDisinviteDifferentRooms(t *testing.T) { assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - if message, ok := client1.RunUntilRoomlistDisinvite(ctx); ok { + if message, err := client1.RunUntilRoomlistDisinvite(ctx); assert.NoError(err) { assert.Equal(roomId1, message.RoomId) } - client1.RunUntilClosed(ctx) + if message, err := client1.RunUntilMessage(ctx); err != nil && !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) { + assert.NoError(err) + } else if err == nil { + assert.Fail("Server should have closed the connection, received %+v", *message) + } - if message, ok := client2.RunUntilRoomlistDisinvite(ctx); ok { + if message, err := client2.RunUntilRoomlistDisinvite(ctx); assert.NoError(err) { assert.Equal(roomId1, message.RoomId) } - msg = &talk.BackendServerRoomRequest{ + msg = &BackendServerRoomRequest{ Type: "update", - Update: &talk.BackendRoomUpdateRequest{ + Update: &BackendRoomUpdateRequest{ UserIds: []string{ testDefaultUserId, }, @@ -629,27 +623,25 @@ func TestBackendServer_RoomDisinviteDifferentRooms(t *testing.T) { assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - if message, ok := client2.RunUntilRoomlistUpdate(ctx); ok { + if message, err := client2.RunUntilRoomlistUpdate(ctx); assert.NoError(err) { assert.Equal(roomId2, message.RoomId) } } func TestBackendServer_RoomUpdate(t *testing.T) { - t.Parallel() - for _, backend := range eventstest.EventBackendsForTest { + CatchLogForTest(t) + for _, backend := range eventBackendsForTest { t.Run(backend, func(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - RunTestBackendServer_RoomUpdate(ctx, t) + RunTestBackendServer_RoomUpdate(t) }) } } -func RunTestBackendServer_RoomUpdate(ctx context.Context, t *testing.T) { +func RunTestBackendServer_RoomUpdate(t *testing.T) { require := require.New(t) assert := assert.New(t) - _, _, asyncEvents, hub, _, server := CreateBackendServerForTest(t) + _, _, events, hub, _, server := CreateBackendServerForTest(t) u, err := url.Parse(server.URL) require.NoError(err) @@ -658,25 +650,23 @@ func RunTestBackendServer_RoomUpdate(ctx context.Context, t *testing.T) { emptyProperties := json.RawMessage("{}") backend := hub.backend.GetBackend(u) require.NotNil(backend, "Did not find backend") - room, err := hub.CreateRoom(roomId, emptyProperties, backend) + room, err := hub.createRoom(roomId, emptyProperties, backend) require.NoError(err, "Could not create room") defer room.Close() userid := "test-userid" roomProperties := json.RawMessage("{\"foo\":\"bar\"}") - eventsChan := make(events.AsyncChannel, 1) + eventsChan := make(chan *AsyncMessage, 1) listener := &channelEventListener{ ch: eventsChan, } - require.NoError(asyncEvents.RegisterUserListener(userid, backend, listener)) - defer func() { - assert.NoError(asyncEvents.UnregisterUserListener(userid, backend, listener)) - }() + require.NoError(events.RegisterUserListener(userid, backend, listener)) + defer events.UnregisterUserListener(userid, backend, listener) - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "update", - Update: &talk.BackendRoomUpdateRequest{ + Update: &BackendRoomUpdateRequest{ UserIds: []string{ userid, }, @@ -693,9 +683,11 @@ func RunTestBackendServer_RoomUpdate(ctx context.Context, t *testing.T) { assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - if event, ok := expectRoomlistEvent(t, eventsChan, "update"); ok && assert.NotNil(event.Update) { - assert.Equal(roomId, event.Update.RoomId) - assert.Equal(string(roomProperties), string(event.Update.Properties)) + if event, err := expectRoomlistEvent(eventsChan, "update"); assert.NoError(err) { + if assert.NotNil(event.Update) { + assert.Equal(roomId, event.Update.RoomId) + assert.Equal(string(roomProperties), string(event.Update.Properties)) + } } // TODO: Use event to wait for asynchronous messages. @@ -707,21 +699,19 @@ func RunTestBackendServer_RoomUpdate(ctx context.Context, t *testing.T) { } func TestBackendServer_RoomDelete(t *testing.T) { - t.Parallel() - for _, backend := range eventstest.EventBackendsForTest { + CatchLogForTest(t) + for _, backend := range eventBackendsForTest { t.Run(backend, func(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - RunTestBackendServer_RoomDelete(ctx, t) + RunTestBackendServer_RoomDelete(t) }) } } -func RunTestBackendServer_RoomDelete(ctx context.Context, t *testing.T) { +func RunTestBackendServer_RoomDelete(t *testing.T) { require := require.New(t) assert := assert.New(t) - _, _, asyncEvents, hub, _, server := CreateBackendServerForTest(t) + _, _, events, hub, _, server := CreateBackendServerForTest(t) u, err := url.Parse(server.URL) require.NoError(err) @@ -730,22 +720,20 @@ func RunTestBackendServer_RoomDelete(ctx context.Context, t *testing.T) { emptyProperties := json.RawMessage("{}") backend := hub.backend.GetBackend(u) require.NotNil(backend, "Did not find backend") - _, err = hub.CreateRoom(roomId, emptyProperties, backend) + _, err = hub.createRoom(roomId, emptyProperties, backend) require.NoError(err) userid := "test-userid" - eventsChan := make(events.AsyncChannel, 1) + eventsChan := make(chan *AsyncMessage, 1) listener := &channelEventListener{ ch: eventsChan, } - require.NoError(asyncEvents.RegisterUserListener(userid, backend, listener)) - defer func() { - assert.NoError(asyncEvents.UnregisterUserListener(userid, backend, listener)) - }() + require.NoError(events.RegisterUserListener(userid, backend, listener)) + defer events.UnregisterUserListener(userid, backend, listener) - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "delete", - Delete: &talk.BackendRoomDeleteRequest{ + Delete: &BackendRoomDeleteRequest{ UserIds: []string{ userid, }, @@ -762,10 +750,12 @@ func RunTestBackendServer_RoomDelete(ctx context.Context, t *testing.T) { assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) // A deleted room is signalled as a "disinvite" event. - if event, ok := expectRoomlistEvent(t, eventsChan, "disinvite"); ok && assert.NotNil(event.Disinvite) { - assert.Equal(roomId, event.Disinvite.RoomId) - assert.Empty(event.Disinvite.Properties) - assert.Equal("deleted", event.Disinvite.Reason) + if event, err := expectRoomlistEvent(eventsChan, "disinvite"); assert.NoError(err) { + if assert.NotNil(event.Disinvite) { + assert.Equal(roomId, event.Disinvite.RoomId) + assert.Empty(event.Disinvite.Properties) + assert.Equal("deleted", event.Disinvite.Reason) + } } // TODO: Use event to wait for asynchronous messages. @@ -776,12 +766,10 @@ func RunTestBackendServer_RoomDelete(ctx context.Context, t *testing.T) { } func TestBackendServer_ParticipantsUpdatePermissions(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) require := require.New(t) assert := assert.New(t) var hub1 *Hub @@ -798,13 +786,20 @@ func TestBackendServer_ParticipantsUpdatePermissions(t *testing.T) { _, _, hub1, hub2, server1, server2 = CreateBackendServerWithClusteringForTest(t) } - ctx, cancel := context.WithTimeout(ctx, testTimeout) + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - defer client1.CloseWithBye() - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") - defer client2.CloseWithBye() + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) session1 := hub1.GetSessionByPublicId(hello1.Hello.SessionId) require.NotNil(session1, "Session %s does not exist", hello1.Hello.SessionId) @@ -812,44 +807,45 @@ func TestBackendServer_ParticipantsUpdatePermissions(t *testing.T) { require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) // Sessions have all permissions initially (fallback for old-style sessions). - assertSessionHasPermission(t, session1, api.PERMISSION_MAY_PUBLISH_MEDIA) - assertSessionHasPermission(t, session1, api.PERMISSION_MAY_PUBLISH_SCREEN) - assertSessionHasPermission(t, session2, api.PERMISSION_MAY_PUBLISH_MEDIA) - assertSessionHasPermission(t, session2, api.PERMISSION_MAY_PUBLISH_SCREEN) + assertSessionHasPermission(t, session1, PERMISSION_MAY_PUBLISH_MEDIA) + assertSessionHasPermission(t, session1, PERMISSION_MAY_PUBLISH_SCREEN) + assertSessionHasPermission(t, session2, PERMISSION_MAY_PUBLISH_MEDIA) + assertSessionHasPermission(t, session2, PERMISSION_MAY_PUBLISH_SCREEN) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) - client1.RunUntilJoined(ctx, hello1.Hello) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Ignore "join" events. - client1.RunUntilJoined(ctx, hello2.Hello) - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client1.DrainMessages(ctx)) + assert.NoError(client2.DrainMessages(ctx)) - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "participants", - Participants: &talk.BackendRoomParticipantsRequest{ - Changed: []api.StringMap{ + Participants: &BackendRoomParticipantsRequest{ + Changed: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), - "permissions": []api.Permission{api.PERMISSION_MAY_PUBLISH_MEDIA}, + "sessionId": roomId + "-" + hello1.Hello.SessionId, + "permissions": []Permission{PERMISSION_MAY_PUBLISH_MEDIA}, }, { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello2.Hello.SessionId), - "permissions": []api.Permission{api.PERMISSION_MAY_PUBLISH_SCREEN}, + "sessionId": roomId + "-" + hello2.Hello.SessionId, + "permissions": []Permission{PERMISSION_MAY_PUBLISH_SCREEN}, }, }, - Users: []api.StringMap{ + Users: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), - "permissions": []api.Permission{api.PERMISSION_MAY_PUBLISH_MEDIA}, + "sessionId": roomId + "-" + hello1.Hello.SessionId, + "permissions": []Permission{PERMISSION_MAY_PUBLISH_MEDIA}, }, { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello2.Hello.SessionId), - "permissions": []api.Permission{api.PERMISSION_MAY_PUBLISH_SCREEN}, + "sessionId": roomId + "-" + hello2.Hello.SessionId, + "permissions": []Permission{PERMISSION_MAY_PUBLISH_SCREEN}, }, }, }, @@ -868,58 +864,62 @@ func TestBackendServer_ParticipantsUpdatePermissions(t *testing.T) { // TODO: Use event to wait for asynchronous messages. time.Sleep(10 * time.Millisecond) - assertSessionHasPermission(t, session1, api.PERMISSION_MAY_PUBLISH_MEDIA) - assertSessionHasNotPermission(t, session1, api.PERMISSION_MAY_PUBLISH_SCREEN) - assertSessionHasNotPermission(t, session2, api.PERMISSION_MAY_PUBLISH_MEDIA) - assertSessionHasPermission(t, session2, api.PERMISSION_MAY_PUBLISH_SCREEN) + assertSessionHasPermission(t, session1, PERMISSION_MAY_PUBLISH_MEDIA) + assertSessionHasNotPermission(t, session1, PERMISSION_MAY_PUBLISH_SCREEN) + assertSessionHasNotPermission(t, session2, PERMISSION_MAY_PUBLISH_MEDIA) + assertSessionHasPermission(t, session2, PERMISSION_MAY_PUBLISH_SCREEN) }) } } func TestBackendServer_ParticipantsUpdateEmptyPermissions(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + require.NoError(client.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - defer client.CloseWithBye() + hello, err := client.RunUntilHello(ctx) + require.NoError(err) session := hub.GetSessionByPublicId(hello.Hello.SessionId) assert.NotNil(session, "Session %s does not exist", hello.Hello.SessionId) // Sessions have all permissions initially (fallback for old-style sessions). - assertSessionHasPermission(t, session, api.PERMISSION_MAY_PUBLISH_MEDIA) - assertSessionHasPermission(t, session, api.PERMISSION_MAY_PUBLISH_SCREEN) + assertSessionHasPermission(t, session, PERMISSION_MAY_PUBLISH_MEDIA) + assertSessionHasPermission(t, session, PERMISSION_MAY_PUBLISH_SCREEN) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Ignore "join" events. - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client.DrainMessages(ctx)) // Updating with empty permissions upgrades to non-old-style and removes // all previously available permissions. - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "participants", - Participants: &talk.BackendRoomParticipantsRequest{ - Changed: []api.StringMap{ + Participants: &BackendRoomParticipantsRequest{ + Changed: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId), - "permissions": []api.Permission{}, + "sessionId": roomId + "-" + hello.Hello.SessionId, + "permissions": []Permission{}, }, }, - Users: []api.StringMap{ + Users: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId), - "permissions": []api.Permission{}, + "sessionId": roomId + "-" + hello.Hello.SessionId, + "permissions": []Permission{}, }, }, }, @@ -937,49 +937,59 @@ func TestBackendServer_ParticipantsUpdateEmptyPermissions(t *testing.T) { // TODO: Use event to wait for asynchronous messages. time.Sleep(10 * time.Millisecond) - assertSessionHasNotPermission(t, session, api.PERMISSION_MAY_PUBLISH_MEDIA) - assertSessionHasNotPermission(t, session, api.PERMISSION_MAY_PUBLISH_SCREEN) + assertSessionHasNotPermission(t, session, PERMISSION_MAY_PUBLISH_MEDIA) + assertSessionHasNotPermission(t, session, PERMISSION_MAY_PUBLISH_SCREEN) } func TestBackendServer_ParticipantsUpdateTimeout(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - defer client1.CloseWithBye() - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - defer client2.CloseWithBye() + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Give message processing some time. time.Sleep(10 * time.Millisecond) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) var wg sync.WaitGroup - wg.Go(func() { - msg := &talk.BackendServerRoomRequest{ + wg.Add(1) + go func() { + defer wg.Done() + msg := &BackendServerRoomRequest{ Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ + InCall: &BackendRoomInCallRequest{ InCall: json.RawMessage("7"), - Changed: []api.StringMap{ + Changed: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), + "sessionId": roomId + "-" + hello1.Hello.SessionId, "inCall": 7, }, { @@ -987,9 +997,9 @@ func TestBackendServer_ParticipantsUpdateTimeout(t *testing.T) { "inCall": 3, }, }, - Users: []api.StringMap{ + Users: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), + "sessionId": roomId + "-" + hello1.Hello.SessionId, "inCall": 7, }, { @@ -1012,33 +1022,35 @@ func TestBackendServer_ParticipantsUpdateTimeout(t *testing.T) { body, err := io.ReadAll(res.Body) assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - }) + }() // Ensure the first request is being processed. time.Sleep(100 * time.Millisecond) - wg.Go(func() { - msg := &talk.BackendServerRoomRequest{ + wg.Add(1) + go func() { + defer wg.Done() + msg := &BackendServerRoomRequest{ Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ + InCall: &BackendRoomInCallRequest{ InCall: json.RawMessage("7"), - Changed: []api.StringMap{ + Changed: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), + "sessionId": roomId + "-" + hello1.Hello.SessionId, "inCall": 7, }, { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello2.Hello.SessionId), + "sessionId": roomId + "-" + hello2.Hello.SessionId, "inCall": 3, }, }, - Users: []api.StringMap{ + Users: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), + "sessionId": roomId + "-" + hello1.Hello.SessionId, "inCall": 7, }, { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello2.Hello.SessionId), + "sessionId": roomId + "-" + hello2.Hello.SessionId, "inCall": 3, }, }, @@ -1057,53 +1069,60 @@ func TestBackendServer_ParticipantsUpdateTimeout(t *testing.T) { body, err := io.ReadAll(res.Body) assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - }) + }() wg.Wait() if t.Failed() { return } - if msg1_a, ok := client1.RunUntilMessage(ctx); ok { - if in_call_1, ok := checkMessageParticipantsInCall(t, msg1_a); ok { - if len(in_call_1.Users) != 2 { - msg1_b, _ := client1.RunUntilMessage(ctx) - if in_call_2, ok := checkMessageParticipantsInCall(t, msg1_b); ok { - assert.Len(in_call_2.Users, 2) - } + msg1_a, err := client1.RunUntilMessage(ctx) + assert.NoError(err) + if in_call_1, err := checkMessageParticipantsInCall(msg1_a); assert.NoError(err) { + if len(in_call_1.Users) != 2 { + msg1_b, err := client1.RunUntilMessage(ctx) + assert.NoError(err) + if in_call_2, err := checkMessageParticipantsInCall(msg1_b); assert.NoError(err) { + assert.Len(in_call_2.Users, 2) } } } - if msg2_a, ok := client2.RunUntilMessage(ctx); ok { - if in_call_1, ok := checkMessageParticipantsInCall(t, msg2_a); ok { - if len(in_call_1.Users) != 2 { - msg2_b, _ := client2.RunUntilMessage(ctx) - if in_call_2, ok := checkMessageParticipantsInCall(t, msg2_b); ok { - assert.Len(in_call_2.Users, 2) - } + msg2_a, err := client2.RunUntilMessage(ctx) + assert.NoError(err) + if in_call_1, err := checkMessageParticipantsInCall(msg2_a); assert.NoError(err) { + if len(in_call_1.Users) != 2 { + msg2_b, err := client2.RunUntilMessage(ctx) + assert.NoError(err) + if in_call_2, err := checkMessageParticipantsInCall(msg2_b); assert.NoError(err) { + assert.Len(in_call_2.Users, 2) } } } - ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) + ctx2, cancel2 := context.WithTimeout(context.Background(), time.Second+100*time.Millisecond) defer cancel2() - client1.RunUntilErrorIs(ctx2, context.DeadlineExceeded) + if msg1_c, _ := client1.RunUntilMessage(ctx2); msg1_c != nil { + if in_call_2, err := checkMessageParticipantsInCall(msg1_c); assert.NoError(err) { + assert.Len(in_call_2.Users, 2) + } + } - ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond) + ctx3, cancel3 := context.WithTimeout(context.Background(), time.Second+100*time.Millisecond) defer cancel3() - - client2.RunUntilErrorIs(ctx3, context.DeadlineExceeded) + if msg2_c, _ := client2.RunUntilMessage(ctx3); msg2_c != nil { + if in_call_2, err := checkMessageParticipantsInCall(msg2_c); assert.NoError(err) { + assert.Len(in_call_2.Users, 2) + } + } } func TestBackendServer_InCallAll(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) require := require.New(t) assert := assert.New(t) var hub1 *Hub @@ -1120,13 +1139,20 @@ func TestBackendServer_InCallAll(t *testing.T) { _, _, hub1, hub2, server1, server2 = CreateBackendServerWithClusteringForTest(t) } - ctx, cancel := context.WithTimeout(ctx, testTimeout) + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - defer client1.CloseWithBye() - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") - defer client2.CloseWithBye() + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) session1 := hub1.GetSessionByPublicId(hello1.Hello.SessionId) require.NotNil(session1, "Could not find session %s", hello1.Hello.SessionId) @@ -1135,13 +1161,15 @@ func TestBackendServer_InCallAll(t *testing.T) { // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Give message processing some time. time.Sleep(10 * time.Millisecond) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) @@ -1155,10 +1183,12 @@ func TestBackendServer_InCallAll(t *testing.T) { assert.False(room2.IsSessionInCall(session2), "Session %s should not be in room %s", session2.PublicId(), room2.Id()) var wg sync.WaitGroup - wg.Go(func() { - msg := &talk.BackendServerRoomRequest{ + wg.Add(1) + go func() { + defer wg.Done() + msg := &BackendServerRoomRequest{ Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ + InCall: &BackendRoomInCallRequest{ InCall: json.RawMessage("7"), All: true, }, @@ -1176,22 +1206,22 @@ func TestBackendServer_InCallAll(t *testing.T) { body, err := io.ReadAll(res.Body) assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - }) + }() wg.Wait() if t.Failed() { return } - if msg1_a, ok := client1.RunUntilMessage(ctx); ok { - if in_call_1, ok := checkMessageParticipantsInCall(t, msg1_a); ok { + if msg1_a, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if in_call_1, err := checkMessageParticipantsInCall(msg1_a); assert.NoError(err) { assert.True(in_call_1.All, "All flag not set in message %+v", in_call_1) assert.Equal("7", string(in_call_1.InCall)) } } - if msg2_a, ok := client2.RunUntilMessage(ctx); ok { - if in_call_1, ok := checkMessageParticipantsInCall(t, msg2_a); ok { + if msg2_a, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + if in_call_1, err := checkMessageParticipantsInCall(msg2_a); assert.NoError(err) { assert.True(in_call_1.All, "All flag not set in message %+v", in_call_1) assert.Equal("7", string(in_call_1.InCall)) } @@ -1203,17 +1233,27 @@ func TestBackendServer_InCallAll(t *testing.T) { ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel2() - client1.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client1.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel3() - client2.RunUntilErrorIs(ctx3, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx3); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } - wg.Go(func() { - msg := &talk.BackendServerRoomRequest{ + wg.Add(1) + go func() { + defer wg.Done() + msg := &BackendServerRoomRequest{ Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ + InCall: &BackendRoomInCallRequest{ InCall: json.RawMessage("0"), All: true, }, @@ -1231,22 +1271,22 @@ func TestBackendServer_InCallAll(t *testing.T) { body, err := io.ReadAll(res.Body) assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - }) + }() wg.Wait() if t.Failed() { return } - if msg1_a, ok := client1.RunUntilMessage(ctx); ok { - if in_call_1, ok := checkMessageParticipantsInCall(t, msg1_a); ok { + if msg1_a, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if in_call_1, err := checkMessageParticipantsInCall(msg1_a); assert.NoError(err) { assert.True(in_call_1.All, "All flag not set in message %+v", in_call_1) assert.Equal("0", string(in_call_1.InCall)) } } - if msg2_a, ok := client2.RunUntilMessage(ctx); ok { - if in_call_1, ok := checkMessageParticipantsInCall(t, msg2_a); ok { + if msg2_a, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + if in_call_1, err := checkMessageParticipantsInCall(msg2_a); assert.NoError(err) { assert.True(in_call_1.All, "All flag not set in message %+v", in_call_1) assert.Equal("0", string(in_call_1.InCall)) } @@ -1258,42 +1298,54 @@ func TestBackendServer_InCallAll(t *testing.T) { ctx4, cancel4 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel4() - client1.RunUntilErrorIs(ctx4, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client1.RunUntilMessage(ctx4); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } ctx5, cancel5 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel5() - client2.RunUntilErrorIs(ctx5, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx5); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } }) } } func TestBackendServer_RoomMessage(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + require.NoError(client.SendHello(testDefaultUserId + "1")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - defer client.CloseWithBye() + _, err := client.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Ignore "join" events. - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client.DrainMessages(ctx)) messageData := json.RawMessage("{\"foo\":\"bar\"}") - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "message", - Message: &talk.BackendRoomMessageRequest{ + Message: &BackendRoomMessageRequest{ Data: messageData, }, } @@ -1307,7 +1359,7 @@ func TestBackendServer_RoomMessage(t *testing.T) { assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - if message, ok := client.RunUntilRoomMessage(ctx); ok { + if message, err := client.RunUntilRoomMessage(ctx); assert.NoError(err) { assert.Equal(roomId, message.RoomId) assert.Equal(string(messageData), string(message.Data)) } @@ -1315,6 +1367,7 @@ func TestBackendServer_RoomMessage(t *testing.T) { func TestBackendServer_TurnCredentials(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, _, _, server := CreateBackendServerForTestWithTurn(t) @@ -1332,19 +1385,19 @@ func TestBackendServer_TurnCredentials(t *testing.T) { assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - var cred talk.TurnCredentials + var cred TurnCredentials require.NoError(json.Unmarshal(body, &cred)) m := hmac.New(sha1.New, []byte(turnSecret)) m.Write([]byte(cred.Username)) // nolint password := base64.StdEncoding.EncodeToString(m.Sum(nil)) assert.Equal(password, cred.Password) - assert.InEpsilon((24 * time.Hour).Seconds(), cred.TTL, 0.0001) + assert.EqualValues((24 * time.Hour).Seconds(), cred.TTL) assert.Equal(turnServers, cred.URIs) } func TestBackendServer_StatsAllowedIps(t *testing.T) { - t.Parallel() + CatchLogForTest(t) config := goconf.NewConfigFile() config.AddOption("app", "trustedproxies", "1.2.3.4") config.AddOption("stats", "allowed_ips", "127.0.0.1, 192.168.0.1, 192.168.1.1/24") @@ -1363,6 +1416,7 @@ func TestBackendServer_StatsAllowedIps(t *testing.T) { } for _, addr := range allowed { + addr := addr t.Run(addr, func(t *testing.T) { t.Parallel() assert := assert.New(t) @@ -1402,6 +1456,7 @@ func TestBackendServer_StatsAllowedIps(t *testing.T) { } for _, addr := range notAllowed { + addr := addr t.Run(addr, func(t *testing.T) { t.Parallel() r := &http.Request{ @@ -1440,8 +1495,7 @@ func Test_IsNumeric(t *testing.T) { func TestBackendServer_DialoutNoSipBridge(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) @@ -1450,15 +1504,16 @@ func TestBackendServer_DialoutNoSipBridge(t *testing.T) { defer client.CloseWithBye() require.NoError(client.SendHelloInternal()) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - MustSucceed1(t, client.RunUntilHello, ctx) + _, err := client.RunUntilHello(ctx) + require.NoError(err) roomId := "12345" - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "dialout", - Dialout: &talk.BackendRoomDialoutRequest{ + Dialout: &BackendRoomDialoutRequest{ Number: "+1234567890", }, } @@ -1472,7 +1527,7 @@ func TestBackendServer_DialoutNoSipBridge(t *testing.T) { assert.NoError(err) require.Equal(http.StatusNotFound, res.StatusCode, "Expected error, got %s", string(body)) - var response talk.BackendServerRoomResponse + var response BackendServerRoomResponse if assert.NoError(json.Unmarshal(body, &response)) { assert.Equal("dialout", response.Type) if assert.NotNil(response.Dialout) && @@ -1484,8 +1539,7 @@ func TestBackendServer_DialoutNoSipBridge(t *testing.T) { func TestBackendServer_DialoutAccepted(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) @@ -1494,10 +1548,11 @@ func TestBackendServer_DialoutAccepted(t *testing.T) { defer client.CloseWithBye() require.NoError(client.SendHelloInternalWithFeatures([]string{"start-dialout"})) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - MustSucceed1(t, client.RunUntilHello, ctx) + _, err := client.RunUntilHello(ctx) + require.NoError(err) roomId := "12345" callId := "call-123" @@ -1506,8 +1561,8 @@ func TestBackendServer_DialoutAccepted(t *testing.T) { go func() { defer close(stopped) - msg, ok := client.RunUntilMessage(ctx) - if !ok { + msg, err := client.RunUntilMessage(ctx) + if !assert.NoError(err) { return } @@ -1521,15 +1576,15 @@ func TestBackendServer_DialoutAccepted(t *testing.T) { assert.Equal(roomId, msg.Internal.Dialout.RoomId) assert.Equal(server.URL+"/", msg.Internal.Dialout.Backend) - response := &api.ClientMessage{ + response := &ClientMessage{ Id: msg.Id, Type: "internal", - Internal: &api.InternalClientMessage{ + Internal: &InternalClientMessage{ Type: "dialout", - Dialout: &api.DialoutInternalClientMessage{ + Dialout: &DialoutInternalClientMessage{ Type: "status", RoomId: msg.Internal.Dialout.RoomId, - Status: &api.DialoutStatusInternalClientMessage{ + Status: &DialoutStatusInternalClientMessage{ Status: "accepted", CallId: callId, }, @@ -1543,9 +1598,9 @@ func TestBackendServer_DialoutAccepted(t *testing.T) { <-stopped }() - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "dialout", - Dialout: &talk.BackendRoomDialoutRequest{ + Dialout: &BackendRoomDialoutRequest{ Number: "+1234567890", }, } @@ -1559,7 +1614,7 @@ func TestBackendServer_DialoutAccepted(t *testing.T) { assert.NoError(err) require.Equal(http.StatusOK, res.StatusCode, "Expected success, got %s", string(body)) - var response talk.BackendServerRoomResponse + var response BackendServerRoomResponse if err := json.Unmarshal(body, &response); assert.NoError(err) { assert.Equal("dialout", response.Type) if assert.NotNil(response.Dialout) { @@ -1571,8 +1626,7 @@ func TestBackendServer_DialoutAccepted(t *testing.T) { func TestBackendServer_DialoutAcceptedCompat(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) @@ -1581,10 +1635,11 @@ func TestBackendServer_DialoutAcceptedCompat(t *testing.T) { defer client.CloseWithBye() require.NoError(client.SendHelloInternalWithFeatures([]string{"start-dialout"})) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - MustSucceed1(t, client.RunUntilHello, ctx) + _, err := client.RunUntilHello(ctx) + require.NoError(err) roomId := "12345" callId := "call-123" @@ -1593,8 +1648,8 @@ func TestBackendServer_DialoutAcceptedCompat(t *testing.T) { go func() { defer close(stopped) - msg, ok := client.RunUntilMessage(ctx) - if !ok { + msg, err := client.RunUntilMessage(ctx) + if !assert.NoError(err) { return } @@ -1608,15 +1663,15 @@ func TestBackendServer_DialoutAcceptedCompat(t *testing.T) { assert.Equal(roomId, msg.Internal.Dialout.RoomId) assert.Equal(server.URL+"/", msg.Internal.Dialout.Backend) - response := &api.ClientMessage{ + response := &ClientMessage{ Id: msg.Id, Type: "internal", - Internal: &api.InternalClientMessage{ + Internal: &InternalClientMessage{ Type: "dialout", - Dialout: &api.DialoutInternalClientMessage{ + Dialout: &DialoutInternalClientMessage{ Type: "status", RoomId: msg.Internal.Dialout.RoomId, - Status: &api.DialoutStatusInternalClientMessage{ + Status: &DialoutStatusInternalClientMessage{ Status: "accepted", CallId: callId, }, @@ -1630,9 +1685,9 @@ func TestBackendServer_DialoutAcceptedCompat(t *testing.T) { <-stopped }() - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "dialout", - Dialout: &talk.BackendRoomDialoutRequest{ + Dialout: &BackendRoomDialoutRequest{ Number: "+1234567890", }, } @@ -1646,7 +1701,7 @@ func TestBackendServer_DialoutAcceptedCompat(t *testing.T) { assert.NoError(err) require.Equal(http.StatusOK, res.StatusCode, "Expected success, got %s", string(body)) - var response talk.BackendServerRoomResponse + var response BackendServerRoomResponse if err := json.Unmarshal(body, &response); assert.NoError(err) { assert.Equal("dialout", response.Type) if assert.NotNil(response.Dialout) { @@ -1658,8 +1713,7 @@ func TestBackendServer_DialoutAcceptedCompat(t *testing.T) { func TestBackendServer_DialoutRejected(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) @@ -1668,10 +1722,11 @@ func TestBackendServer_DialoutRejected(t *testing.T) { defer client.CloseWithBye() require.NoError(client.SendHelloInternalWithFeatures([]string{"start-dialout"})) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - MustSucceed1(t, client.RunUntilHello, ctx) + _, err := client.RunUntilHello(ctx) + require.NoError(err) roomId := "12345" errorCode := "error-code" @@ -1681,8 +1736,8 @@ func TestBackendServer_DialoutRejected(t *testing.T) { go func() { defer close(stopped) - msg, ok := client.RunUntilMessage(ctx) - if !ok { + msg, err := client.RunUntilMessage(ctx) + if !assert.NoError(err) { return } @@ -1696,14 +1751,14 @@ func TestBackendServer_DialoutRejected(t *testing.T) { assert.Equal(roomId, msg.Internal.Dialout.RoomId) assert.Equal(server.URL+"/", msg.Internal.Dialout.Backend) - response := &api.ClientMessage{ + response := &ClientMessage{ Id: msg.Id, Type: "internal", - Internal: &api.InternalClientMessage{ + Internal: &InternalClientMessage{ Type: "dialout", - Dialout: &api.DialoutInternalClientMessage{ + Dialout: &DialoutInternalClientMessage{ Type: "error", - Error: api.NewError(errorCode, errorMessage), + Error: NewError(errorCode, errorMessage), }, }, } @@ -1714,9 +1769,9 @@ func TestBackendServer_DialoutRejected(t *testing.T) { <-stopped }() - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "dialout", - Dialout: &talk.BackendRoomDialoutRequest{ + Dialout: &BackendRoomDialoutRequest{ Number: "+1234567890", }, } @@ -1730,7 +1785,7 @@ func TestBackendServer_DialoutRejected(t *testing.T) { assert.NoError(err) require.Equal(http.StatusBadGateway, res.StatusCode, "Expected error, got %s", string(body)) - var response talk.BackendServerRoomResponse + var response BackendServerRoomResponse if err := json.Unmarshal(body, &response); assert.NoError(err) { assert.Equal("dialout", response.Type) if assert.NotNil(response.Dialout) && @@ -1740,116 +1795,3 @@ func TestBackendServer_DialoutRejected(t *testing.T) { } } } - -func TestBackendServer_DialoutFirstFailed(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - require := require.New(t) - assert := assert.New(t) - _, _, _, hub, _, server := CreateBackendServerForTest(t) - - client1 := NewTestClient(t, server, hub) - defer client1.CloseWithBye() - require.NoError(client1.SendHelloInternalWithFeatures([]string{"start-dialout"})) - - client2 := NewTestClient(t, server, hub) - defer client2.CloseWithBye() - require.NoError(client2.SendHelloInternalWithFeatures([]string{"start-dialout"})) - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - MustSucceed1(t, client1.RunUntilHello, ctx) - MustSucceed1(t, client2.RunUntilHello, ctx) - - roomId := "12345" - callId := "call-123" - - var returnedError atomic.Bool - - var wg sync.WaitGroup - runClient := func(client *TestClient) { - msg, ok := client.RunUntilMessage(ctx) - if !ok { - return - } - - if !assert.Equal("internal", msg.Type) || - !assert.NotNil(msg.Internal) || - !assert.Equal("dialout", msg.Internal.Type) || - !assert.NotNil(msg.Internal.Dialout) { - return - } - - assert.Equal(roomId, msg.Internal.Dialout.RoomId) - assert.Equal(server.URL+"/", msg.Internal.Dialout.Backend) - - var dialout *api.DialoutInternalClientMessage - // The first session should return an error to make sure the second is retried afterwards. - if returnedError.CompareAndSwap(false, true) { - errorCode := "error-code" - errorMessage := "rejected call" - - dialout = &api.DialoutInternalClientMessage{ - Type: "error", - Error: api.NewError(errorCode, errorMessage), - } - } else { - dialout = &api.DialoutInternalClientMessage{ - Type: "status", - RoomId: msg.Internal.Dialout.RoomId, - Status: &api.DialoutStatusInternalClientMessage{ - Status: "accepted", - CallId: callId, - }, - } - } - - response := &api.ClientMessage{ - Id: msg.Id, - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "dialout", - Dialout: dialout, - }, - } - assert.NoError(client.WriteJSON(response)) - } - - wg.Go(func() { - runClient(client1) - }) - wg.Go(func() { - runClient(client2) - }) - - defer func() { - wg.Wait() - }() - - msg := &talk.BackendServerRoomRequest{ - Type: "dialout", - Dialout: &talk.BackendRoomDialoutRequest{ - Number: "+1234567890", - }, - } - - data, err := json.Marshal(msg) - require.NoError(err) - res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data) - require.NoError(err) - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - assert.NoError(err) - require.Equal(http.StatusOK, res.StatusCode, "Expected success, got %s", string(body)) - - var response talk.BackendServerRoomResponse - if err := json.Unmarshal(body, &response); assert.NoError(err) { - assert.Equal("dialout", response.Type) - if assert.NotNil(response.Dialout) { - assert.Nil(response.Dialout.Error, "expected dialout success, got %s", string(body)) - assert.Equal(callId, response.Dialout.CallId) - } - } -} diff --git a/talk/backend_storage_etcd.go b/backend_storage_etcd.go similarity index 52% rename from talk/backend_storage_etcd.go rename to backend_storage_etcd.go index caf30af..ce82bef 100644 --- a/talk/backend_storage_etcd.go +++ b/backend_storage_etcd.go @@ -19,57 +19,44 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "context" "encoding/json" "errors" + "fmt" + "log" "net/url" - "slices" - "sync" - "sync/atomic" "time" "github.com/dlintw/goconf" clientv3 "go.etcd.io/etcd/client/v3" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" -) - -const ( - initialWaitDelay = time.Second - maxWaitDelay = 8 * time.Second ) type backendStorageEtcd struct { backendStorageCommon - logger log.Logger - etcdClient etcd.Client + etcdClient *EtcdClient keyPrefix string - keyInfos map[string]*etcd.BackendInformationEtcd + keyInfos map[string]*BackendInformationEtcd - initializing atomic.Bool initializedCtx context.Context initializedFunc context.CancelFunc wakeupChanForTesting chan struct{} - runningDone sync.WaitGroup closeCtx context.Context closeFunc context.CancelFunc } -func NewBackendStorageEtcd(logger log.Logger, config *goconf.ConfigFile, etcdClient etcd.Client, stats BackendStorageStats) (BackendStorage, error) { +func NewBackendStorageEtcd(config *goconf.ConfigFile, etcdClient *EtcdClient) (BackendStorage, error) { if etcdClient == nil || !etcdClient.IsConfigured() { - return nil, errors.New("no etcd endpoints configured") + return nil, fmt.Errorf("no etcd endpoints configured") } keyPrefix, _ := config.GetString("backend", "backendprefix") if keyPrefix == "" { - return nil, errors.New("no backend prefix configured") + return nil, fmt.Errorf("no backend prefix configured") } initializedCtx, initializedFunc := context.WithCancel(context.Background()) @@ -77,12 +64,10 @@ func NewBackendStorageEtcd(logger log.Logger, config *goconf.ConfigFile, etcdCli result := &backendStorageEtcd{ backendStorageCommon: backendStorageCommon{ backends: make(map[string][]*Backend), - stats: stats, }, - logger: logger, etcdClient: etcdClient, keyPrefix: keyPrefix, - keyInfos: make(map[string]*etcd.BackendInformationEtcd), + keyInfos: make(map[string]*BackendInformationEtcd), initializedCtx: initializedCtx, initializedFunc: initializedFunc, @@ -114,16 +99,8 @@ func (s *backendStorageEtcd) wakeupForTesting() { } } -func (s *backendStorageEtcd) EtcdClientCreated(client etcd.Client) { - s.initializing.Store(true) - if s.closeCtx.Err() != nil { - // Stopped before etcd client was connected. - s.initializedFunc() - return - } - - s.runningDone.Go(func() { - defer s.initializedFunc() +func (s *backendStorageEtcd) EtcdClientCreated(client *EtcdClient) { + go func() { if err := client.WaitForConnection(s.closeCtx); err != nil { if errors.Is(err, context.Canceled) { return @@ -132,7 +109,7 @@ func (s *backendStorageEtcd) EtcdClientCreated(client etcd.Client) { panic(err) } - backoff, err := async.NewExponentialBackoff(initialWaitDelay, maxWaitDelay) + backoff, err := NewExponentialBackoff(initialWaitDelay, maxWaitDelay) if err != nil { panic(err) } @@ -142,9 +119,9 @@ func (s *backendStorageEtcd) EtcdClientCreated(client etcd.Client) { if errors.Is(err, context.Canceled) { return } else if errors.Is(err, context.DeadlineExceeded) { - s.logger.Printf("Timeout getting initial list of backends, retry in %s", backoff.NextWait()) + log.Printf("Timeout getting initial list of backends, retry in %s", backoff.NextWait()) } else { - s.logger.Printf("Could not get initial list of backends, retry in %s: %s", backoff.NextWait(), err) + log.Printf("Could not get initial list of backends, retry in %s: %s", backoff.NextWait(), err) } backoff.Wait(s.closeCtx) @@ -162,7 +139,7 @@ func (s *backendStorageEtcd) EtcdClientCreated(client etcd.Client) { for s.closeCtx.Err() == nil { var err error if nextRevision, err = client.Watch(s.closeCtx, s.keyPrefix, nextRevision, s, clientv3.WithPrefix()); err != nil { - s.logger.Printf("Error processing watch for %s (%s), retry in %s", s.keyPrefix, err, backoff.NextWait()) + log.Printf("Error processing watch for %s (%s), retry in %s", s.keyPrefix, err, backoff.NextWait()) backoff.Wait(s.closeCtx) continue } @@ -171,80 +148,89 @@ func (s *backendStorageEtcd) EtcdClientCreated(client etcd.Client) { backoff.Reset() prevRevision = nextRevision } else { - s.logger.Printf("Processing watch for %s interrupted, retry in %s", s.keyPrefix, backoff.NextWait()) + log.Printf("Processing watch for %s interrupted, retry in %s", s.keyPrefix, backoff.NextWait()) backoff.Wait(s.closeCtx) } } return } - }) + }() } -func (s *backendStorageEtcd) EtcdWatchCreated(client etcd.Client, key string) { +func (s *backendStorageEtcd) EtcdWatchCreated(client *EtcdClient, key string) { } -func (s *backendStorageEtcd) getBackends(ctx context.Context, client etcd.Client, keyPrefix string) (*clientv3.GetResponse, error) { +func (s *backendStorageEtcd) getBackends(ctx context.Context, client *EtcdClient, keyPrefix string) (*clientv3.GetResponse, error) { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() return client.Get(ctx, keyPrefix, clientv3.WithPrefix()) } -func (s *backendStorageEtcd) EtcdKeyUpdated(client etcd.Client, key string, data []byte, prevValue []byte) { - var info etcd.BackendInformationEtcd +func (s *backendStorageEtcd) EtcdKeyUpdated(client *EtcdClient, key string, data []byte, prevValue []byte) { + var info BackendInformationEtcd if err := json.Unmarshal(data, &info); err != nil { - s.logger.Printf("Could not decode backend information %s: %s", string(data), err) + log.Printf("Could not decode backend information %s: %s", string(data), err) return } if err := info.CheckValid(); err != nil { - s.logger.Printf("Received invalid backend information %s: %s", string(data), err) + log.Printf("Received invalid backend information %s: %s", string(data), err) return } - backend := NewBackendFromEtcd(key, &info) + backend := &Backend{ + id: key, + url: info.Url, + parsedUrl: info.parsedUrl, + secret: []byte(info.Secret), + + allowHttp: info.parsedUrl.Scheme == "http", + + maxStreamBitrate: info.MaxStreamBitrate, + maxScreenBitrate: info.MaxScreenBitrate, + sessionLimit: info.SessionLimit, + } + + host := info.parsedUrl.Host s.mu.Lock() defer s.mu.Unlock() s.keyInfos[key] = &info - added := false - for idx, u := range info.ParsedUrls { - host := u.Host - entries, found := s.backends[host] - if !found { - // Simple case, first backend for this host - s.logger.Printf("Added backend %s (from %s)", info.Urls[idx], key) - s.backends[host] = []*Backend{backend} - added = true - continue - } + entries, found := s.backends[host] + if !found { + // Simple case, first backend for this host + log.Printf("Added backend %s (from %s)", info.Url, key) + s.backends[host] = []*Backend{backend} + updateBackendStats(backend) + statsBackendsCurrent.Inc() + s.wakeupForTesting() + return + } - // Was the backend changed? - replaced := false - for idx, entry := range entries { - if entry.Id() == key { - s.logger.Printf("Updated backend %s (from %s)", info.Urls[idx], key) - entries[idx] = backend - replaced = true - break - } - } - - if !replaced { - // New backend, add to list. - s.logger.Printf("Added backend %s (from %s)", info.Urls[idx], key) - s.backends[host] = append(entries, backend) - added = true + // Was the backend changed? + replaced := false + for idx, entry := range entries { + if entry.id == key { + log.Printf("Updated backend %s (from %s)", info.Url, key) + updateBackendStats(backend) + entries[idx] = backend + replaced = true + break } } - backend.UpdateStats() - if added { - s.stats.IncBackends() + + if !replaced { + // New backend, add to list. + log.Printf("Added backend %s (from %s)", info.Url, key) + s.backends[host] = append(entries, backend) + updateBackendStats(backend) + statsBackendsCurrent.Inc() } s.wakeupForTesting() } -func (s *backendStorageEtcd) EtcdKeyDeleted(client etcd.Client, key string, prevValue []byte) { +func (s *backendStorageEtcd) EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte) { s.mu.Lock() defer s.mu.Unlock() @@ -254,61 +240,34 @@ func (s *backendStorageEtcd) EtcdKeyDeleted(client etcd.Client, key string, prev } delete(s.keyInfos, key) - var deleted map[string][]*Backend - seen := make(map[string]bool) - for idx, u := range info.ParsedUrls { - host := u.Host - entries, found := s.backends[host] - if !found { - if d, ok := deleted[host]; ok { - if slices.ContainsFunc(d, func(b *Backend) bool { - return slices.Contains(b.Urls(), u.String()) - }) { - s.logger.Printf("Removing backend %s (from %s)", info.Urls[idx], key) - } - } + host := info.parsedUrl.Host + entries, found := s.backends[host] + if !found { + return + } + + log.Printf("Removing backend %s (from %s)", info.Url, key) + newEntries := make([]*Backend, 0, len(entries)-1) + for _, entry := range entries { + if entry.id == key { + updateBackendStats(entry) + statsBackendsCurrent.Dec() continue } - s.logger.Printf("Removing backend %s (from %s)", info.Urls[idx], key) - newEntries := make([]*Backend, 0, len(entries)-1) - for _, entry := range entries { - if entry.Id() == key { - if len(info.ParsedUrls) > 1 { - if deleted == nil { - deleted = make(map[string][]*Backend) - } - deleted[host] = append(deleted[host], entry) - } - if !seen[entry.Id()] { - seen[entry.Id()] = true - entry.UpdateStats() - s.stats.DecBackends() - } - continue - } - - newEntries = append(newEntries, entry) - } - if len(newEntries) > 0 { - s.backends[host] = newEntries - } else { - delete(s.backends, host) - } + newEntries = append(newEntries, entry) + } + if len(newEntries) > 0 { + s.backends[host] = newEntries + } else { + delete(s.backends, host) } s.wakeupForTesting() } func (s *backendStorageEtcd) Close() { - firstStop := s.closeCtx.Err() == nil - s.closeFunc() s.etcdClient.RemoveListener(s) - if firstStop { - if s.initializing.Load() { - <-s.initializedCtx.Done() - } - s.runningDone.Wait() - } + s.closeFunc() } func (s *backendStorageEtcd) Reload(config *goconf.ConfigFile) { diff --git a/talk/backend_storage_etcd_test.go b/backend_storage_etcd_test.go similarity index 70% rename from talk/backend_storage_etcd_test.go rename to backend_storage_etcd_test.go index b00feb0..d9bd77c 100644 --- a/talk/backend_storage_etcd_test.go +++ b/backend_storage_etcd_test.go @@ -19,18 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "testing" "github.com/dlintw/goconf" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" + "go.etcd.io/etcd/server/v3/embed" ) func (s *backendStorageEtcd) getWakeupChannelForTesting() <-chan struct{} { @@ -47,20 +43,21 @@ func (s *backendStorageEtcd) getWakeupChannelForTesting() <-chan struct{} { } type testListener struct { - etcd *etcdtest.Server + etcd *embed.Etcd closed chan struct{} } -func (tl *testListener) EtcdClientCreated(client etcd.Client) { +func (tl *testListener) EtcdClientCreated(client *EtcdClient) { + tl.etcd.Server.Stop() close(tl.closed) } -func Test_BackendStorageEtcdNoLeak(t *testing.T) { // nolint:paralleltest - logger := logtest.NewLoggerForTest(t) - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - embedEtcd, client := etcdtest.NewClientForTest(t) +func Test_BackendStorageEtcdNoLeak(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { + etcd, client := NewEtcdClientForTest(t) tl := &testListener{ - etcd: embedEtcd, + etcd: etcd, closed: make(chan struct{}), } client.AddListener(tl) @@ -70,7 +67,7 @@ func Test_BackendStorageEtcdNoLeak(t *testing.T) { // nolint:paralleltest config.AddOption("backend", "backendtype", "etcd") config.AddOption("backend", "backendprefix", "/backends") - cfg, err := NewBackendConfiguration(logger, config, client) + cfg, err := NewBackendConfiguration(config, client) require.NoError(t, err) <-tl.closed diff --git a/backend_storage_static.go b/backend_storage_static.go new file mode 100644 index 0000000..4a60c3f --- /dev/null +++ b/backend_storage_static.go @@ -0,0 +1,321 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "log" + "net/url" + "reflect" + "strings" + + "github.com/dlintw/goconf" +) + +type backendStorageStatic struct { + backendStorageCommon + + // Deprecated + allowAll bool + commonSecret []byte + compatBackend *Backend +} + +func NewBackendStorageStatic(config *goconf.ConfigFile) (BackendStorage, error) { + allowAll, _ := config.GetBool("backend", "allowall") + allowHttp, _ := config.GetBool("backend", "allowhttp") + commonSecret, _ := config.GetString("backend", "secret") + sessionLimit, err := config.GetInt("backend", "sessionlimit") + if err != nil || sessionLimit < 0 { + sessionLimit = 0 + } + backends := make(map[string][]*Backend) + var compatBackend *Backend + numBackends := 0 + if allowAll { + log.Println("WARNING: All backend hostnames are allowed, only use for development!") + compatBackend = &Backend{ + id: "compat", + secret: []byte(commonSecret), + compat: true, + + allowHttp: allowHttp, + + sessionLimit: uint64(sessionLimit), + } + if sessionLimit > 0 { + log.Printf("Allow a maximum of %d sessions", sessionLimit) + } + updateBackendStats(compatBackend) + numBackends++ + } else if backendIds, _ := config.GetString("backend", "backends"); backendIds != "" { + for host, configuredBackends := range getConfiguredHosts(backendIds, config, commonSecret) { + backends[host] = append(backends[host], configuredBackends...) + for _, be := range configuredBackends { + log.Printf("Backend %s added for %s", be.id, be.url) + updateBackendStats(be) + } + numBackends += len(configuredBackends) + } + } else if allowedUrls, _ := config.GetString("backend", "allowed"); allowedUrls != "" { + // Old-style configuration, only hosts are configured and are using a common secret. + allowMap := make(map[string]bool) + for _, u := range strings.Split(allowedUrls, ",") { + u = strings.TrimSpace(u) + if idx := strings.IndexByte(u, '/'); idx != -1 { + log.Printf("WARNING: Removing path from allowed hostname \"%s\", check your configuration!", u) + u = u[:idx] + } + if u != "" { + allowMap[strings.ToLower(u)] = true + } + } + + if len(allowMap) == 0 { + log.Println("WARNING: No backend hostnames are allowed, check your configuration!") + } else { + compatBackend = &Backend{ + id: "compat", + secret: []byte(commonSecret), + compat: true, + + allowHttp: allowHttp, + + sessionLimit: uint64(sessionLimit), + } + hosts := make([]string, 0, len(allowMap)) + for host := range allowMap { + hosts = append(hosts, host) + backends[host] = []*Backend{compatBackend} + } + if len(hosts) > 1 { + log.Println("WARNING: Using deprecated backend configuration. Please migrate the \"allowed\" setting to the new \"backends\" configuration.") + } + log.Printf("Allowed backend hostnames: %s", hosts) + if sessionLimit > 0 { + log.Printf("Allow a maximum of %d sessions", sessionLimit) + } + updateBackendStats(compatBackend) + numBackends++ + } + } + + if numBackends == 0 { + log.Printf("WARNING: No backends configured, client connections will not be possible.") + } + + statsBackendsCurrent.Add(float64(numBackends)) + return &backendStorageStatic{ + backendStorageCommon: backendStorageCommon{ + backends: backends, + }, + + allowAll: allowAll, + commonSecret: []byte(commonSecret), + compatBackend: compatBackend, + }, nil +} + +func (s *backendStorageStatic) Close() { +} + +func (s *backendStorageStatic) RemoveBackendsForHost(host string) { + if oldBackends := s.backends[host]; len(oldBackends) > 0 { + for _, backend := range oldBackends { + log.Printf("Backend %s removed for %s", backend.id, backend.url) + deleteBackendStats(backend) + } + statsBackendsCurrent.Sub(float64(len(oldBackends))) + } + delete(s.backends, host) +} + +func (s *backendStorageStatic) UpsertHost(host string, backends []*Backend) { + for existingIndex, existingBackend := range s.backends[host] { + found := false + index := 0 + for _, newBackend := range backends { + if reflect.DeepEqual(existingBackend, newBackend) { // otherwise we could manually compare the struct members here + found = true + backends = append(backends[:index], backends[index+1:]...) + break + } else if newBackend.id == existingBackend.id { + found = true + s.backends[host][existingIndex] = newBackend + backends = append(backends[:index], backends[index+1:]...) + log.Printf("Backend %s updated for %s", newBackend.id, newBackend.url) + updateBackendStats(newBackend) + break + } + index++ + } + if !found { + removed := s.backends[host][existingIndex] + log.Printf("Backend %s removed for %s", removed.id, removed.url) + s.backends[host] = append(s.backends[host][:existingIndex], s.backends[host][existingIndex+1:]...) + deleteBackendStats(removed) + statsBackendsCurrent.Dec() + } + } + + s.backends[host] = append(s.backends[host], backends...) + for _, added := range backends { + log.Printf("Backend %s added for %s", added.id, added.url) + updateBackendStats(added) + } + statsBackendsCurrent.Add(float64(len(backends))) +} + +func getConfiguredBackendIDs(backendIds string) (ids []string) { + seen := make(map[string]bool) + + for _, id := range strings.Split(backendIds, ",") { + id = strings.TrimSpace(id) + if id == "" { + continue + } + + if seen[id] { + continue + } + ids = append(ids, id) + seen[id] = true + } + + return ids +} + +func getConfiguredHosts(backendIds string, config *goconf.ConfigFile, commonSecret string) (hosts map[string][]*Backend) { + hosts = make(map[string][]*Backend) + for _, id := range getConfiguredBackendIDs(backendIds) { + u, _ := config.GetString(id, "url") + if u == "" { + log.Printf("Backend %s is missing or incomplete, skipping", id) + continue + } + + if u[len(u)-1] != '/' { + u += "/" + } + parsed, err := url.Parse(u) + if err != nil { + log.Printf("Backend %s has an invalid url %s configured (%s), skipping", id, u, err) + continue + } + + if strings.Contains(parsed.Host, ":") && hasStandardPort(parsed) { + parsed.Host = parsed.Hostname() + u = parsed.String() + } + + secret, _ := config.GetString(id, "secret") + if secret == "" && commonSecret != "" { + log.Printf("Backend %s has no own shared secret set, using common shared secret", id) + secret = commonSecret + } + if u == "" || secret == "" { + log.Printf("Backend %s is missing or incomplete, skipping", id) + continue + } + + sessionLimit, err := config.GetInt(id, "sessionlimit") + if err != nil || sessionLimit < 0 { + sessionLimit = 0 + } + if sessionLimit > 0 { + log.Printf("Backend %s allows a maximum of %d sessions", id, sessionLimit) + } + + maxStreamBitrate, err := config.GetInt(id, "maxstreambitrate") + if err != nil || maxStreamBitrate < 0 { + maxStreamBitrate = 0 + } + maxScreenBitrate, err := config.GetInt(id, "maxscreenbitrate") + if err != nil || maxScreenBitrate < 0 { + maxScreenBitrate = 0 + } + + hosts[parsed.Host] = append(hosts[parsed.Host], &Backend{ + id: id, + url: u, + parsedUrl: parsed, + secret: []byte(secret), + + allowHttp: parsed.Scheme == "http", + + maxStreamBitrate: maxStreamBitrate, + maxScreenBitrate: maxScreenBitrate, + + sessionLimit: uint64(sessionLimit), + }) + } + + return hosts +} + +func (s *backendStorageStatic) Reload(config *goconf.ConfigFile) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.compatBackend != nil { + log.Println("Old-style configuration active, reload is not supported") + return + } + + commonSecret, _ := config.GetString("backend", "secret") + + if backendIds, _ := config.GetString("backend", "backends"); backendIds != "" { + configuredHosts := getConfiguredHosts(backendIds, config, commonSecret) + + // remove backends that are no longer configured + for hostname := range s.backends { + if _, ok := configuredHosts[hostname]; !ok { + s.RemoveBackendsForHost(hostname) + } + } + + // rewrite backends adding newly configured ones and rewriting existing ones + for hostname, configuredBackends := range configuredHosts { + s.UpsertHost(hostname, configuredBackends) + } + } +} + +func (s *backendStorageStatic) GetCompatBackend() *Backend { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.compatBackend +} + +func (s *backendStorageStatic) GetBackend(u *url.URL) *Backend { + s.mu.RLock() + defer s.mu.RUnlock() + + if _, found := s.backends[u.Host]; !found { + if s.allowAll { + return s.compatBackend + } + return nil + } + + return s.getBackendLocked(u) +} diff --git a/async/backoff.go b/backoff.go similarity index 87% rename from async/backoff.go rename to backoff.go index 4d6953d..5b49521 100644 --- a/async/backoff.go +++ b/backoff.go @@ -19,11 +19,11 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package async +package signaling import ( "context" - "errors" + "fmt" "time" ) @@ -41,10 +41,10 @@ type exponentialBackoff struct { func NewExponentialBackoff(initial time.Duration, maxWait time.Duration) (Backoff, error) { if initial <= 0 { - return nil, errors.New("initial must be larger than 0") + return nil, fmt.Errorf("initial must be larger than 0") } if maxWait < initial { - return nil, errors.New("maxWait must be larger or equal to initial") + return nil, fmt.Errorf("maxWait must be larger or equal to initial") } return &exponentialBackoff{ @@ -67,6 +67,10 @@ func (b *exponentialBackoff) Wait(ctx context.Context) { waiter, cancel := context.WithTimeout(ctx, b.nextWait) defer cancel() - b.nextWait = min(b.nextWait*2, b.maxWait) + b.nextWait = b.nextWait * 2 + if b.nextWait > b.maxWait { + b.nextWait = b.maxWait + } + <-waiter.Done() } diff --git a/async/backoff_test.go b/backoff_test.go similarity index 64% rename from async/backoff_test.go rename to backoff_test.go index 7ade659..87de048 100644 --- a/async/backoff_test.go +++ b/backoff_test.go @@ -19,12 +19,11 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package async +package signaling import ( "context" "testing" - "testing/synctest" "time" "github.com/stretchr/testify/assert" @@ -33,32 +32,31 @@ import ( func TestBackoff_Exponential(t *testing.T) { t.Parallel() - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - minWait := 100 * time.Millisecond - backoff, err := NewExponentialBackoff(minWait, 500*time.Millisecond) - require.NoError(t, err) + assert := assert.New(t) + minWait := 100 * time.Millisecond + backoff, err := NewExponentialBackoff(minWait, 500*time.Millisecond) + require.NoError(t, err) - waitTimes := []time.Duration{ - minWait, - 200 * time.Millisecond, - 400 * time.Millisecond, - 500 * time.Millisecond, - 500 * time.Millisecond, - } + waitTimes := []time.Duration{ + minWait, + 200 * time.Millisecond, + 400 * time.Millisecond, + 500 * time.Millisecond, + 500 * time.Millisecond, + } - for _, wait := range waitTimes { - assert.Equal(wait, backoff.NextWait()) - a := time.Now() - backoff.Wait(context.Background()) - b := time.Now() - assert.Equal(b.Sub(a), wait) - } + for _, wait := range waitTimes { + assert.Equal(wait, backoff.NextWait()) - backoff.Reset() a := time.Now() backoff.Wait(context.Background()) b := time.Now() - assert.Equal(b.Sub(a), minWait) - }) + assert.GreaterOrEqual(b.Sub(a), wait) + } + + backoff.Reset() + a := time.Now() + backoff.Wait(context.Background()) + b := time.Now() + assert.GreaterOrEqual(b.Sub(a), minWait) } diff --git a/talk/capabilities.go b/capabilities.go similarity index 71% rename from talk/capabilities.go rename to capabilities.go index 4df1267..e606bf0 100644 --- a/talk/capabilities.go +++ b/capabilities.go @@ -19,23 +19,21 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "context" "encoding/json" "errors" + "io" + "log" "net/http" "net/url" "strings" "sync" "time" - "github.com/pquerna/cachecontrol/cacheobject" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/pool" + "github.com/marcw/cachecontrol" ) const ( @@ -58,22 +56,16 @@ const ( ) var ( - ErrUnexpectedHttpStatus = errors.New("unexpected_http_status") // +checklocksignore: Global readonly variable. + ErrUnexpectedHttpStatus = errors.New("unexpected_http_status") ) type capabilitiesEntry struct { - mu sync.RWMutex - // +checklocks:mu - c *Capabilities - - // +checklocks:mu - nextUpdate time.Time - // +checklocks:mu - etag string - // +checklocks:mu + c *Capabilities + mu sync.RWMutex + nextUpdate time.Time + etag string mustRevalidate bool - // +checklocks:mu - capabilities api.StringMap + capabilities map[string]interface{} } func newCapabilitiesEntry(c *Capabilities) *capabilitiesEntry { @@ -89,12 +81,10 @@ func (e *capabilitiesEntry) valid(now time.Time) bool { return e.validLocked(now) } -// +checklocksread:e.mu func (e *capabilitiesEntry) validLocked(now time.Time) bool { return e.nextUpdate.After(now) } -// +checklocks:e.mu func (e *capabilitiesEntry) updateRequest(r *http.Request) { if e.etag != "" { r.Header.Set("If-None-Match", e.etag) @@ -108,7 +98,6 @@ func (e *capabilitiesEntry) invalidate() { e.nextUpdate = time.Now() } -// +checklocks:e.mu func (e *capabilitiesEntry) errorIfMustRevalidate(err error) (bool, error) { if !e.mustRevalidate { return false, nil @@ -119,7 +108,6 @@ func (e *capabilitiesEntry) errorIfMustRevalidate(err error) (bool, error) { } func (e *capabilitiesEntry) update(ctx context.Context, u *url.URL, now time.Time) (bool, error) { - logger := log.LoggerFromContext(ctx) e.mu.Lock() defer e.mu.Unlock() @@ -133,23 +121,23 @@ func (e *capabilitiesEntry) update(ctx context.Context, u *url.URL, now time.Tim if !strings.HasSuffix(capUrl.Path, "/") { capUrl.Path += "/" } - capUrl.Path += "ocs/v2.php/cloud/capabilities" + capUrl.Path = capUrl.Path + "ocs/v2.php/cloud/capabilities" } else if pos := strings.Index(capUrl.Path, "/ocs/v2.php/"); pos >= 0 { capUrl.Path = capUrl.Path[:pos+11] + "/cloud/capabilities" } - logger.Printf("Capabilities expired for %s, updating", capUrl.String()) + log.Printf("Capabilities expired for %s, updating", capUrl.String()) client, pool, err := e.c.pool.Get(ctx, &capUrl) if err != nil { - logger.Printf("Could not get client for host %s: %s", capUrl.Host, err) + log.Printf("Could not get client for host %s: %s", capUrl.Host, err) return false, err } defer pool.Put(client) req, err := http.NewRequestWithContext(ctx, "GET", capUrl.String(), nil) if err != nil { - logger.Printf("Could not create request to %s: %s", &capUrl, err) + log.Printf("Could not create request to %s: %s", &capUrl, err) return false, err } req.Header.Set("Accept", "application/json") @@ -168,75 +156,74 @@ func (e *capabilitiesEntry) update(ctx context.Context, u *url.URL, now time.Tim var maxAge time.Duration if cacheControl := response.Header.Get("Cache-Control"); cacheControl != "" { - if cc, err := cacheobject.ParseResponseCacheControl(cacheControl); err == nil { - if !cc.NoCachePresent && cc.MaxAge > 0 { - maxAge = time.Duration(cc.MaxAge) * time.Second - } - e.mustRevalidate = cc.MustRevalidate + cc := cachecontrol.Parse(cacheControl) + if nc, _ := cc.NoCache(); !nc { + maxAge = cc.MaxAge() } - } - if maxAge < minCapabilitiesCacheDuration { + if maxAge < minCapabilitiesCacheDuration { + maxAge = minCapabilitiesCacheDuration + } + e.mustRevalidate = cc.MustRevalidate() + } else { maxAge = minCapabilitiesCacheDuration } e.nextUpdate = now.Add(maxAge) if response.StatusCode == http.StatusNotModified { - logger.Printf("Capabilities %+v from %s have not changed", e.capabilities, url) + log.Printf("Capabilities %+v from %s have not changed", e.capabilities, url) return false, nil } else if response.StatusCode != http.StatusOK { - logger.Printf("Received unexpected HTTP status from %s: %s", url, response.Status) + log.Printf("Received unexpected HTTP status from %s: %s", url, response.Status) return e.errorIfMustRevalidate(ErrUnexpectedHttpStatus) } ct := response.Header.Get("Content-Type") if !strings.HasPrefix(ct, "application/json") { - logger.Printf("Received unsupported content-type from %s: %s (%s)", url, ct, response.Status) + log.Printf("Received unsupported content-type from %s: %s (%s)", url, ct, response.Status) return e.errorIfMustRevalidate(ErrUnsupportedContentType) } - body, err := e.c.buffers.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) if err != nil { - logger.Printf("Could not read response body from %s: %s", url, err) + log.Printf("Could not read response body from %s: %s", url, err) return e.errorIfMustRevalidate(err) } - defer e.c.buffers.Put(body) - var ocs OcsResponse - if err := json.Unmarshal(body.Bytes(), &ocs); err != nil { - logger.Printf("Could not decode OCS response %s from %s: %s", body.String(), url, err) + if err := json.Unmarshal(body, &ocs); err != nil { + log.Printf("Could not decode OCS response %s from %s: %s", string(body), url, err) return e.errorIfMustRevalidate(err) } else if ocs.Ocs == nil || len(ocs.Ocs.Data) == 0 { - logger.Printf("Incomplete OCS response %s from %s", body.String(), url) + log.Printf("Incomplete OCS response %s from %s", string(body), url) return e.errorIfMustRevalidate(ErrIncompleteResponse) } var capaResponse CapabilitiesResponse if err := json.Unmarshal(ocs.Ocs.Data, &capaResponse); err != nil { - logger.Printf("Could not decode OCS response body %s from %s: %s", string(ocs.Ocs.Data), url, err) + log.Printf("Could not decode OCS response body %s from %s: %s", string(ocs.Ocs.Data), url, err) return e.errorIfMustRevalidate(err) } capaObj, found := capaResponse.Capabilities[AppNameSpreed] if !found || len(capaObj) == 0 { - logger.Printf("No capabilities received for app spreed from %s: %+v", url, capaResponse) + log.Printf("No capabilities received for app spreed from %s: %+v", url, capaResponse) e.capabilities = nil return false, nil } - var capa api.StringMap + var capa map[string]interface{} if err := json.Unmarshal(capaObj, &capa); err != nil { - logger.Printf("Unsupported capabilities received for app spreed from %s: %+v", url, capaResponse) + log.Printf("Unsupported capabilities received for app spreed from %s: %+v", url, capaResponse) e.capabilities = nil return false, nil } - logger.Printf("Received capabilities %+v from %s", capa, url) + log.Printf("Received capabilities %+v from %s", capa, url) e.capabilities = capa return true, nil } -func (e *capabilitiesEntry) GetCapabilities() api.StringMap { +func (e *capabilitiesEntry) GetCapabilities() map[string]interface{} { e.mu.RLock() defer e.mu.RUnlock() @@ -249,17 +236,13 @@ type Capabilities struct { // Can be overwritten by tests. getNow func() time.Time - version string - pool *pool.HttpClientPool - // +checklocks:mu - entries map[string]*capabilitiesEntry - // +checklocks:mu + version string + pool *HttpClientPool + entries map[string]*capabilitiesEntry nextInvalidate map[string]time.Time - - buffers pool.BufferPool } -func NewCapabilities(version string, pool *pool.HttpClientPool) (*Capabilities, error) { +func NewCapabilities(version string, pool *HttpClientPool) (*Capabilities, error) { result := &Capabilities{ getNow: time.Now, @@ -337,7 +320,7 @@ func (c *Capabilities) getKeyForUrl(u *url.URL) string { return key } -func (c *Capabilities) loadCapabilities(ctx context.Context, u *url.URL) (api.StringMap, bool, error) { +func (c *Capabilities) loadCapabilities(ctx context.Context, u *url.URL) (map[string]interface{}, bool, error) { key := c.getKeyForUrl(u) entry, valid := c.getCapabilities(key) if valid { @@ -353,10 +336,9 @@ func (c *Capabilities) loadCapabilities(ctx context.Context, u *url.URL) (api.St } func (c *Capabilities) HasCapabilityFeature(ctx context.Context, u *url.URL, feature string) bool { - logger := log.LoggerFromContext(ctx) caps, _, err := c.loadCapabilities(ctx, u) if err != nil { - logger.Printf("Could not get capabilities for %s: %s", u, err) + log.Printf("Could not get capabilities for %s: %s", u, err) return false } @@ -365,9 +347,9 @@ func (c *Capabilities) HasCapabilityFeature(ctx context.Context, u *url.URL, fea return false } - features, ok := featuresInterface.([]any) + features, ok := featuresInterface.([]interface{}) if !ok { - logger.Printf("Invalid features list received for %s: %+v", u, featuresInterface) + log.Printf("Invalid features list received for %s: %+v", u, featuresInterface) return false } @@ -379,11 +361,10 @@ func (c *Capabilities) HasCapabilityFeature(ctx context.Context, u *url.URL, fea return false } -func (c *Capabilities) getConfigGroup(ctx context.Context, u *url.URL, group string) (api.StringMap, bool, bool) { - logger := log.LoggerFromContext(ctx) +func (c *Capabilities) getConfigGroup(ctx context.Context, u *url.URL, group string) (map[string]interface{}, bool, bool) { caps, cached, err := c.loadCapabilities(ctx, u) if err != nil { - logger.Printf("Could not get capabilities for %s: %s", u, err) + log.Printf("Could not get capabilities for %s: %s", u, err) return nil, cached, false } @@ -392,9 +373,9 @@ func (c *Capabilities) getConfigGroup(ctx context.Context, u *url.URL, group str return nil, cached, false } - config, ok := api.ConvertStringMap(configInterface) + config, ok := configInterface.(map[string]interface{}) if !ok { - logger.Printf("Invalid config mapping received from %s: %+v", u, configInterface) + log.Printf("Invalid config mapping received from %s: %+v", u, configInterface) return nil, cached, false } @@ -403,9 +384,9 @@ func (c *Capabilities) getConfigGroup(ctx context.Context, u *url.URL, group str return nil, cached, false } - groupConfig, ok := api.ConvertStringMap(groupInterface) + groupConfig, ok := groupInterface.(map[string]interface{}) if !ok { - logger.Printf("Invalid group mapping \"%s\" received from %s: %+v", group, u, groupInterface) + log.Printf("Invalid group mapping \"%s\" received from %s: %+v", group, u, groupInterface) return nil, cached, false } @@ -431,8 +412,7 @@ func (c *Capabilities) GetIntegerConfig(ctx context.Context, u *url.URL, group, case float64: return int(value), cached, true default: - logger := log.LoggerFromContext(ctx) - logger.Printf("Invalid config value for \"%s\" received from %s: %+v", key, u, value) + log.Printf("Invalid config value for \"%s\" received from %s: %+v", key, u, value) } return 0, cached, false @@ -453,8 +433,7 @@ func (c *Capabilities) GetStringConfig(ctx context.Context, u *url.URL, group, k case string: return value, cached, true default: - logger := log.LoggerFromContext(ctx) - logger.Printf("Invalid config value for \"%s\" received from %s: %+v", key, u, value) + log.Printf("Invalid config value for \"%s\" received from %s: %+v", key, u, value) } return "", cached, false diff --git a/talk/capabilities_test.go b/capabilities_test.go similarity index 88% rename from talk/capabilities_test.go rename to capabilities_test.go index 8e2ab17..d8857b4 100644 --- a/talk/capabilities_test.go +++ b/capabilities_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "context" @@ -40,21 +40,11 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/pool" -) - -const ( - testTimeout = 10 * time.Second ) func NewCapabilitiesForTestWithCallback(t *testing.T, callback func(*CapabilitiesResponse, http.ResponseWriter) error) (*url.URL, *Capabilities) { require := require.New(t) - assert := assert.New(t) - pool, err := pool.NewHttpClientPool(1, false) + pool, err := NewHttpClientPool(1, false) require.NoError(err) capabilities, err := NewCapabilities("0.0", pool) require.NoError(err) @@ -76,18 +66,17 @@ func NewCapabilitiesForTestWithCallback(t *testing.T, callback func(*Capabilitie if strings.Contains(t.Name(), "V3Api") { features = append(features, "signaling-v3") } - signaling := api.StringMap{ + signaling := map[string]interface{}{ "foo": "bar", "baz": 42, } - config := api.StringMap{ + config := map[string]interface{}{ "signaling": signaling, } - spreedCapa, err := json.Marshal(api.StringMap{ + spreedCapa, _ := json.Marshal(map[string]interface{}{ "features": features, "config": config, }) - assert.NoError(err) emptyArray := []byte("[]") response := &CapabilitiesResponse{ Version: CapabilitiesVersion{ @@ -100,7 +89,7 @@ func NewCapabilitiesForTestWithCallback(t *testing.T, callback func(*Capabilitie } data, err := json.Marshal(response) - assert.NoError(err, "Could not marshal %+v", response) + assert.NoError(t, err, "Could not marshal %+v", response) var ocs OcsResponse ocs.Ocs = &OcsBody{ @@ -112,7 +101,7 @@ func NewCapabilitiesForTestWithCallback(t *testing.T, callback func(*Capabilitie Data: data, } data, err = json.Marshal(ocs) - assert.NoError(err) + require.NoError(err) var cc []string if !strings.Contains(t.Name(), "NoCache") { @@ -183,12 +172,11 @@ func SetCapabilitiesGetNow(t *testing.T, capabilities *Capabilities, f func() ti func TestCapabilities(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) url, capabilities := NewCapabilitiesForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() assert.True(capabilities.HasCapabilityFeature(ctx, url, "foo")) @@ -227,8 +215,7 @@ func TestCapabilities(t *testing.T) { func TestInvalidateCapabilities(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) var called atomic.Uint32 url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse, w http.ResponseWriter) error { @@ -236,7 +223,7 @@ func TestInvalidateCapabilities(t *testing.T) { return nil }) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() expectedString := "bar" @@ -288,8 +275,7 @@ func TestInvalidateCapabilities(t *testing.T) { func TestCapabilitiesNoCache(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) var called atomic.Uint32 url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse, w http.ResponseWriter) error { @@ -297,7 +283,7 @@ func TestCapabilitiesNoCache(t *testing.T) { return nil }) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() expectedString := "bar" @@ -333,8 +319,7 @@ func TestCapabilitiesNoCache(t *testing.T) { func TestCapabilitiesShortCache(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) var called atomic.Uint32 url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse, w http.ResponseWriter) error { @@ -342,7 +327,7 @@ func TestCapabilitiesShortCache(t *testing.T) { return nil }) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() expectedString := "bar" @@ -363,7 +348,7 @@ func TestCapabilitiesShortCache(t *testing.T) { value = called.Load() assert.EqualValues(1, value) - // The capabilities are cached for a minimum duration. + // The capabilities are cached for a minumum duration. SetCapabilitiesGetNow(t, capabilities, func() time.Time { return time.Now().Add(minCapabilitiesCacheDuration / 2) }) @@ -388,8 +373,7 @@ func TestCapabilitiesShortCache(t *testing.T) { func TestCapabilitiesNoCacheETag(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) var called atomic.Uint32 url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse, w http.ResponseWriter) error { @@ -403,7 +387,7 @@ func TestCapabilitiesNoCacheETag(t *testing.T) { return nil }) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() expectedString := "bar" @@ -430,8 +414,7 @@ func TestCapabilitiesNoCacheETag(t *testing.T) { func TestCapabilitiesCacheNoMustRevalidate(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) var called atomic.Uint32 url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse, w http.ResponseWriter) error { @@ -442,7 +425,7 @@ func TestCapabilitiesCacheNoMustRevalidate(t *testing.T) { return nil }) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() expectedString := "bar" @@ -471,8 +454,7 @@ func TestCapabilitiesCacheNoMustRevalidate(t *testing.T) { func TestCapabilitiesNoCacheNoMustRevalidate(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) var called atomic.Uint32 url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse, w http.ResponseWriter) error { @@ -483,7 +465,7 @@ func TestCapabilitiesNoCacheNoMustRevalidate(t *testing.T) { return nil }) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() expectedString := "bar" @@ -512,8 +494,7 @@ func TestCapabilitiesNoCacheNoMustRevalidate(t *testing.T) { func TestCapabilitiesNoCacheMustRevalidate(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) var called atomic.Uint32 url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse, w http.ResponseWriter) error { @@ -524,7 +505,7 @@ func TestCapabilitiesNoCacheMustRevalidate(t *testing.T) { return nil }) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() expectedString := "bar" @@ -551,8 +532,7 @@ func TestCapabilitiesNoCacheMustRevalidate(t *testing.T) { func TestConcurrentExpired(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) var called atomic.Uint32 url, capabilities := NewCapabilitiesForTestWithCallback(t, func(cr *CapabilitiesResponse, w http.ResponseWriter) error { @@ -560,7 +540,7 @@ func TestConcurrentExpired(t *testing.T) { return nil }) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() expectedString := "bar" @@ -574,8 +554,10 @@ func TestConcurrentExpired(t *testing.T) { var numCached atomic.Uint32 var numFetched atomic.Uint32 var finished sync.WaitGroup - for range count { - finished.Go(func() { + for i := 0; i < count; i++ { + finished.Add(1) + go func() { + defer finished.Done() <-start if value, cached, found := capabilities.GetStringConfig(ctx, url, "signaling", "foo"); assert.True(found) { assert.Equal(expectedString, value) @@ -585,7 +567,7 @@ func TestConcurrentExpired(t *testing.T) { numFetched.Add(1) } } - }) + }() } SetCapabilitiesGetNow(t, capabilities, func() time.Time { diff --git a/security/certificate_reloader.go b/certificate_reloader.go similarity index 70% rename from security/certificate_reloader.go rename to certificate_reloader.go index fa14cdf..3e23c96 100644 --- a/security/certificate_reloader.go +++ b/certificate_reloader.go @@ -19,56 +19,45 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package security +package signaling import ( "crypto/tls" "crypto/x509" "fmt" + "log" "os" "sync/atomic" - "testing" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/security/internal" ) type CertificateReloader struct { - logger log.Logger - certFile string - certWatcher *internal.FileWatcher + certWatcher *FileWatcher keyFile string - keyWatcher *internal.FileWatcher + keyWatcher *FileWatcher certificate atomic.Pointer[tls.Certificate] reloadCounter atomic.Uint64 } -func NewCertificateReloader(logger log.Logger, certFile string, keyFile string) (*CertificateReloader, error) { +func NewCertificateReloader(certFile string, keyFile string) (*CertificateReloader, error) { pair, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf("could not load certificate / key: %w", err) } - deduplicate := internal.DefaultDeduplicateWatchEvents - if testing.Testing() { - deduplicate = 0 - } - reloader := &CertificateReloader{ - logger: logger, certFile: certFile, keyFile: keyFile, } reloader.certificate.Store(&pair) - reloader.certWatcher, err = internal.NewFileWatcher(reloader.logger, certFile, reloader.reload, deduplicate) + reloader.certWatcher, err = NewFileWatcher(certFile, reloader.reload) if err != nil { return nil, err } - reloader.keyWatcher, err = internal.NewFileWatcher(reloader.logger, keyFile, reloader.reload, deduplicate) + reloader.keyWatcher, err = NewFileWatcher(keyFile, reloader.reload) if err != nil { reloader.certWatcher.Close() // nolint return nil, err @@ -83,10 +72,10 @@ func (r *CertificateReloader) Close() { } func (r *CertificateReloader) reload(filename string) { - r.logger.Printf("reloading certificate from %s with %s", r.certFile, r.keyFile) + log.Printf("reloading certificate from %s with %s", r.certFile, r.keyFile) pair, err := tls.LoadX509KeyPair(r.certFile, r.keyFile) if err != nil { - r.logger.Printf("could not load certificate / key: %s", err) + log.Printf("could not load certificate / key: %s", err) return } @@ -111,10 +100,8 @@ func (r *CertificateReloader) GetReloadCounter() uint64 { } type CertPoolReloader struct { - logger log.Logger - certFile string - certWatcher *internal.FileWatcher + certWatcher *FileWatcher pool atomic.Pointer[x509.CertPool] @@ -135,23 +122,17 @@ func loadCertPool(filename string) (*x509.CertPool, error) { return pool, nil } -func NewCertPoolReloader(logger log.Logger, certFile string) (*CertPoolReloader, error) { +func NewCertPoolReloader(certFile string) (*CertPoolReloader, error) { pool, err := loadCertPool(certFile) if err != nil { return nil, err } - deduplicate := internal.DefaultDeduplicateWatchEvents - if testing.Testing() { - deduplicate = 0 - } - reloader := &CertPoolReloader{ - logger: logger, certFile: certFile, } reloader.pool.Store(pool) - reloader.certWatcher, err = internal.NewFileWatcher(reloader.logger, certFile, reloader.reload, deduplicate) + reloader.certWatcher, err = NewFileWatcher(certFile, reloader.reload) if err != nil { return nil, err } @@ -164,10 +145,10 @@ func (r *CertPoolReloader) Close() { } func (r *CertPoolReloader) reload(filename string) { - r.logger.Printf("reloading certificate pool from %s", r.certFile) + log.Printf("reloading certificate pool from %s", r.certFile) pool, err := loadCertPool(r.certFile) if err != nil { - r.logger.Printf("could not load certificate pool: %s", err) + log.Printf("could not load certificate pool: %s", err) return } diff --git a/dns/test/dns.go b/certificate_reloader_test.go similarity index 52% rename from dns/test/dns.go rename to certificate_reloader_test.go index dd71a99..a180a9d 100644 --- a/dns/test/dns.go +++ b/certificate_reloader_test.go @@ -1,6 +1,6 @@ /** * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG + * Copyright (C) 2022 struktur AG * * @author Joachim Bauch * @@ -19,41 +19,44 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package test +package signaling import ( + "context" "testing" "time" - - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns/internal" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" ) -type MockLookup = internal.MockLookup - -func NewMockLookup() *MockLookup { - return internal.NewMockLookup() -} - -func NewMonitorForTest(t *testing.T, interval time.Duration, lookup *MockLookup) *dns.Monitor { +func UpdateCertificateCheckIntervalForTest(t *testing.T, interval time.Duration) { t.Helper() - require := require.New(t) - - logger := logtest.NewLoggerForTest(t) - var lookupFunc dns.MonitorLookupFunc - if lookup != nil { - lookupFunc = lookup.Lookup - } - monitor, err := dns.NewMonitor(logger, interval, lookupFunc) - require.NoError(err) - + // Make sure test is not executed with "t.Parallel()" + t.Setenv("PARALLEL_CHECK", "1") + old := deduplicateWatchEvents.Load() t.Cleanup(func() { - monitor.Stop() + deduplicateWatchEvents.Store(old) }) - require.NoError(monitor.Start()) - return monitor + deduplicateWatchEvents.Store(int64(interval)) +} + +func (r *CertificateReloader) WaitForReload(ctx context.Context) error { + counter := r.GetReloadCounter() + for counter == r.GetReloadCounter() { + if err := ctx.Err(); err != nil { + return err + } + time.Sleep(time.Millisecond) + } + return nil +} + +func (r *CertPoolReloader) WaitForReload(ctx context.Context) error { + counter := r.GetReloadCounter() + for counter == r.GetReloadCounter() { + if err := ctx.Err(); err != nil { + return err + } + time.Sleep(time.Millisecond) + } + return nil } diff --git a/test/wakeup_channel.go b/channel_waiter.go similarity index 56% rename from test/wakeup_channel.go rename to channel_waiter.go index 39476be..20b0883 100644 --- a/test/wakeup_channel.go +++ b/channel_waiter.go @@ -1,6 +1,6 @@ /** * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG + * Copyright (C) 2023 struktur AG * * @author Joachim Bauch * @@ -19,14 +19,44 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package test +package signaling -func DrainWakeupChannel(ch <-chan struct{}) { - for { +import ( + "sync" +) + +type ChannelWaiters struct { + mu sync.RWMutex + id uint64 + waiters map[uint64]chan struct{} +} + +func (w *ChannelWaiters) Wakeup() { + w.mu.RLock() + defer w.mu.RUnlock() + for _, ch := range w.waiters { select { - case <-ch: + case ch <- struct{}{}: default: - return + // Receiver is still processing previous wakeup. } } } + +func (w *ChannelWaiters) Add(ch chan struct{}) uint64 { + w.mu.Lock() + defer w.mu.Unlock() + if w.waiters == nil { + w.waiters = make(map[uint64]chan struct{}) + } + id := w.id + w.id++ + w.waiters[id] = ch + return id +} + +func (w *ChannelWaiters) Remove(id uint64) { + w.mu.Lock() + defer w.mu.Unlock() + delete(w.waiters, id) +} diff --git a/dns/internal/mock_lookup_test.go b/channel_waiter_test.go similarity index 54% rename from dns/internal/mock_lookup_test.go rename to channel_waiter_test.go index 9bcf7c8..9141642 100644 --- a/dns/internal/mock_lookup_test.go +++ b/channel_waiter_test.go @@ -1,6 +1,6 @@ /** * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG + * Copyright (C) 2023 struktur AG * * @author Joachim Bauch * @@ -19,40 +19,50 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( - "net" "testing" "github.com/stretchr/testify/assert" ) -func TestMockLookup(t *testing.T) { - t.Parallel() +func TestChannelWaiters(t *testing.T) { + var waiters ChannelWaiters - assert := assert.New(t) + ch1 := make(chan struct{}, 1) + id1 := waiters.Add(ch1) + defer waiters.Remove(id1) - host1 := "domain1.invalid" - host2 := "domain2.invalid" + ch2 := make(chan struct{}, 1) + id2 := waiters.Add(ch2) + defer waiters.Remove(id2) - lookup := NewMockLookup() - assert.Empty(lookup.Get(host1)) - assert.Empty(lookup.Get(host2)) + waiters.Wakeup() + <-ch1 + <-ch2 - ips := []net.IP{ - net.ParseIP("1.2.3.4"), + select { + case <-ch1: + assert.Fail(t, "should have not received another event") + case <-ch2: + assert.Fail(t, "should have not received another event") + default: } - lookup.Set(host1, ips) - assert.Equal(ips, lookup.Get(host1)) - assert.Empty(lookup.Get(host2)) - if resolved, err := lookup.Lookup(host1); assert.NoError(err) { - assert.Equal(ips, resolved) - } - var de *net.DNSError - if resolved, err := lookup.Lookup(host2); assert.ErrorAs(err, &de, "expected error, got %+v", resolved) { - assert.True(de.IsNotFound) - assert.Equal(host2, de.Name) + ch3 := make(chan struct{}, 1) + id3 := waiters.Add(ch3) + waiters.Remove(id3) + + // Multiple wakeups work even without processing. + waiters.Wakeup() + waiters.Wakeup() + waiters.Wakeup() + <-ch1 + <-ch2 + select { + case <-ch3: + assert.Fail(t, "should have not received another event") + default: } } diff --git a/client/client.go b/client.go similarity index 59% rename from client/client.go rename to client.go index 7f53bb0..3980218 100644 --- a/client/client.go +++ b/client.go @@ -19,14 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package client +package signaling import ( "bytes" "context" "encoding/json" "errors" - "io" + "log" "net" "strconv" "strings" @@ -36,12 +36,6 @@ import ( "github.com/gorilla/websocket" "github.com/mailru/easyjson" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/pool" ) const ( @@ -58,112 +52,136 @@ const ( maxMessageSize = 64 * 1024 ) +var ( + noCountry = "no-country" + + loopback = "loopback" + + unknownCountry = "unknown-country" +) + func init() { RegisterClientStats() } -var ( - InvalidFormat = api.NewError("invalid_format", "Invalid data format.") +func IsValidCountry(country string) bool { + switch country { + case "": + fallthrough + case noCountry: + fallthrough + case loopback: + fallthrough + case unknownCountry: + return false + default: + return true + } +} - bufferPool pool.BufferPool +var ( + InvalidFormat = NewError("invalid_format", "Invalid data format.") + + bufferPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, + } ) type WritableClientMessage interface { json.Marshaler - CloseAfterSend(session api.RoomAware) bool + CloseAfterSend(session Session) bool } type HandlerClient interface { Context() context.Context RemoteAddr() string - Country() geoip.Country + Country() string UserAgent() string IsConnected() bool + IsAuthenticated() bool - SendError(e *api.Error) bool - SendByeResponse(message *api.ClientMessage) bool - SendByeResponseWithReason(message *api.ClientMessage, reason string) bool + GetSession() Session + SetSession(session Session) + + SendError(e *Error) bool + SendByeResponse(message *ClientMessage) bool + SendByeResponseWithReason(message *ClientMessage, reason string) bool SendMessage(message WritableClientMessage) bool Close() } -type Handler interface { - GetSessionId() api.PublicSessionId - - OnClosed() - OnMessageReceived([]byte) - OnRTTReceived(time.Duration) +type ClientHandler interface { + OnClosed(HandlerClient) + OnMessageReceived(HandlerClient, []byte) + OnRTTReceived(HandlerClient, time.Duration) } -type GeoIpHandler interface { - OnLookupCountry(addr string) geoip.Country -} - -type InRoomHandler interface { - IsInRoom(string) bool -} - -type SessionCloserHandler interface { - CloseSession() +type ClientGeoIpHandler interface { + OnLookupCountry(HandlerClient) string } type Client struct { - logger log.Logger - ctx context.Context - // +checklocks:mu + ctx context.Context conn *websocket.Conn addr string agent string closed atomic.Int32 - country *geoip.Country + country *string logRTT bool handlerMu sync.RWMutex - // +checklocks:handlerMu - handler Handler + handler ClientHandler - sessionId atomic.Pointer[api.PublicSessionId] + session atomic.Pointer[Session] + sessionId atomic.Pointer[string] mu sync.Mutex - closer *internal.Closer + closer *Closer closeOnce sync.Once messagesDone chan struct{} messageChan chan *bytes.Buffer } -func (c *Client) SetConn(ctx context.Context, conn *websocket.Conn, remoteAddress string, agent string, logRTT bool, handler Handler) { - c.mu.Lock() - defer c.mu.Unlock() +func NewClient(ctx context.Context, conn *websocket.Conn, remoteAddress string, agent string, handler ClientHandler) (*Client, error) { + remoteAddress = strings.TrimSpace(remoteAddress) + if remoteAddress == "" { + remoteAddress = "unknown remote address" + } + agent = strings.TrimSpace(agent) + if agent == "" { + agent = "unknown user agent" + } - c.logger = log.LoggerFromContext(ctx) + client := &Client{ + agent: agent, + logRTT: true, + } + client.SetConn(ctx, conn, remoteAddress, handler) + return client, nil +} + +func (c *Client) SetConn(ctx context.Context, conn *websocket.Conn, remoteAddress string, handler ClientHandler) { c.ctx = ctx c.conn = conn c.addr = remoteAddress - c.agent = agent - c.logRTT = logRTT c.SetHandler(handler) - c.closer = internal.NewCloser() + c.closer = NewCloser() c.messageChan = make(chan *bytes.Buffer, 16) c.messagesDone = make(chan struct{}) } -func (c *Client) GetConn() *websocket.Conn { - c.mu.Lock() - defer c.mu.Unlock() - - return c.conn -} - -func (c *Client) SetHandler(handler Handler) { +func (c *Client) SetHandler(handler ClientHandler) { c.handlerMu.Lock() defer c.handlerMu.Unlock() c.handler = handler } -func (c *Client) getHandler() Handler { +func (c *Client) getHandler() ClientHandler { c.handlerMu.RLock() defer c.handlerMu.RUnlock() return c.handler @@ -177,19 +195,40 @@ func (c *Client) IsConnected() bool { return c.closed.Load() == 0 } -func (c *Client) SetSessionId(sessionId api.PublicSessionId) { +func (c *Client) IsAuthenticated() bool { + return c.GetSession() != nil +} + +func (c *Client) GetSession() Session { + session := c.session.Load() + if session == nil { + return nil + } + + return *session +} + +func (c *Client) SetSession(session Session) { + if session == nil { + c.session.Store(nil) + } else { + c.session.Store(&session) + } +} + +func (c *Client) SetSessionId(sessionId string) { c.sessionId.Store(&sessionId) } -func (c *Client) GetSessionId() api.PublicSessionId { +func (c *Client) GetSessionId() string { sessionId := c.sessionId.Load() if sessionId == nil { - sessionId := c.getHandler().GetSessionId() - if sessionId == "" { + session := c.GetSession() + if session == nil { return "" } - return sessionId + return session.PublicId() } return *sessionId @@ -203,13 +242,13 @@ func (c *Client) UserAgent() string { return c.agent } -func (c *Client) Country() geoip.Country { +func (c *Client) Country() string { if c.country == nil { - var country geoip.Country - if handler, ok := c.getHandler().(GeoIpHandler); ok { - country = handler.OnLookupCountry(c.addr) + var country string + if handler, ok := c.getHandler().(ClientGeoIpHandler); ok { + country = handler.OnLookupCountry(c) } else { - country = geoip.UnknownCountry + country = unknownCountry } c.country = &country } @@ -217,14 +256,6 @@ func (c *Client) Country() geoip.Country { return *c.country } -func (c *Client) IsInRoom(id string) bool { - if handler, ok := c.getHandler().(InRoomHandler); ok { - return handler.IsInRoom(id) - } - - return false -} - func (c *Client) Close() { if c.closed.Load() >= 2 { // Prevent reentrant call in case this was the second closing @@ -240,8 +271,7 @@ func (c *Client) Close() { func (c *Client) doClose() { closed := c.closed.Add(1) - switch closed { - case 1: + if closed == 1 { c.mu.Lock() defer c.mu.Unlock() if c.conn != nil { @@ -249,29 +279,30 @@ func (c *Client) doClose() { c.conn.Close() c.conn = nil } - case 2: + } else if closed == 2 { // Both the read pump and message processing must be finished before closing. c.closer.Close() <-c.messagesDone - c.getHandler().OnClosed() + c.getHandler().OnClosed(c) + c.SetSession(nil) } } -func (c *Client) SendError(e *api.Error) bool { - message := &api.ServerMessage{ +func (c *Client) SendError(e *Error) bool { + message := &ServerMessage{ Type: "error", Error: e, } return c.SendMessage(message) } -func (c *Client) SendByeResponse(message *api.ClientMessage) bool { +func (c *Client) SendByeResponse(message *ClientMessage) bool { return c.SendByeResponseWithReason(message, "") } -func (c *Client) SendByeResponseWithReason(message *api.ClientMessage, reason string) bool { - response := &api.ServerMessage{ +func (c *Client) SendByeResponseWithReason(message *ClientMessage, reason string) bool { + response := &ServerMessage{ Type: "bye", } if message != nil { @@ -279,7 +310,7 @@ func (c *Client) SendByeResponseWithReason(message *api.ClientMessage, reason st } if reason != "" { if response.Bye == nil { - response.Bye = &api.ByeServerMessage{} + response.Bye = &ByeServerMessage{} } response.Bye.Reason = reason } @@ -303,7 +334,7 @@ func (c *Client) ReadPump() { conn := c.conn c.mu.Unlock() if conn == nil { - c.logger.Printf("Connection from %s closed while starting readPump", addr) + log.Printf("Connection from %s closed while starting readPump", addr) return } @@ -314,19 +345,17 @@ func (c *Client) ReadPump() { if msg == "" { return nil } - statsClientBytesTotal.WithLabelValues("incoming").Add(float64(len(msg))) if ts, err := strconv.ParseInt(msg, 10, 64); err == nil { rtt := now.Sub(time.Unix(0, ts)) if c.logRTT { rtt_ms := rtt.Nanoseconds() / time.Millisecond.Nanoseconds() if sessionId := c.GetSessionId(); sessionId != "" { - c.logger.Printf("Client %s has RTT of %d ms (%s)", sessionId, rtt_ms, rtt) + log.Printf("Client %s has RTT of %d ms (%s)", sessionId, rtt_ms, rtt) } else { - c.logger.Printf("Client from %s has RTT of %d ms (%s)", addr, rtt_ms, rtt) + log.Printf("Client from %s has RTT of %d ms (%s)", addr, rtt_ms, rtt) } } - statsClientRTT.Observe(float64(rtt.Milliseconds())) - c.getHandler().OnRTTReceived(rtt) + c.getHandler().OnRTTReceived(c, rtt) } return nil }) @@ -343,9 +372,9 @@ func (c *Client) ReadPump() { websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { if sessionId := c.GetSessionId(); sessionId != "" { - c.logger.Printf("Error reading from client %s: %v", sessionId, err) + log.Printf("Error reading from client %s: %v", sessionId, err) } else { - c.logger.Printf("Error reading from %s: %v", addr, err) + log.Printf("Error reading from %s: %v", addr, err) } } break @@ -353,20 +382,22 @@ func (c *Client) ReadPump() { if messageType != websocket.TextMessage { if sessionId := c.GetSessionId(); sessionId != "" { - c.logger.Printf("Unsupported message type %v from client %s", messageType, sessionId) + log.Printf("Unsupported message type %v from client %s", messageType, sessionId) } else { - c.logger.Printf("Unsupported message type %v from %s", messageType, addr) + log.Printf("Unsupported message type %v from %s", messageType, addr) } c.SendError(InvalidFormat) continue } - decodeBuffer, err := bufferPool.ReadAll(reader) - if err != nil { + decodeBuffer := bufferPool.Get().(*bytes.Buffer) + decodeBuffer.Reset() + if _, err := decodeBuffer.ReadFrom(reader); err != nil { + bufferPool.Put(decodeBuffer) if sessionId := c.GetSessionId(); sessionId != "" { - c.logger.Printf("Error reading message from client %s: %v", sessionId, err) + log.Printf("Error reading message from client %s: %v", sessionId, err) } else { - c.logger.Printf("Error reading message from %s: %v", addr, err) + log.Printf("Error reading message from %s: %v", addr, err) } break } @@ -377,8 +408,6 @@ func (c *Client) ReadPump() { break } - statsClientBytesTotal.WithLabelValues("incoming").Add(float64(decodeBuffer.Len())) - statsClientMessagesTotal.WithLabelValues("incoming").Inc() c.messageChan <- decodeBuffer } } @@ -390,7 +419,7 @@ func (c *Client) processMessages() { break } - c.getHandler().OnMessageReceived(buffer.Bytes()) + c.getHandler().OnMessageReceived(c, buffer.Bytes()) bufferPool.Put(buffer) } @@ -398,34 +427,16 @@ func (c *Client) processMessages() { c.doClose() } -type counterWriter struct { - w io.Writer - counter *int -} - -func (w *counterWriter) Write(p []byte) (int, error) { - written, err := w.w.Write(p) - if written > 0 { - *w.counter += written - } - return written, err -} - -// +checklocks:c.mu func (c *Client) writeInternal(message json.Marshaler) bool { var closeData []byte c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint writer, err := c.conn.NextWriter(websocket.TextMessage) - var written int if err == nil { - if m, ok := (any(message)).(easyjson.Marshaler); ok { - written, err = easyjson.MarshalToWriter(m, writer) + if m, ok := (interface{}(message)).(easyjson.Marshaler); ok { + _, err = easyjson.MarshalToWriter(m, writer) } else { - err = json.NewEncoder(&counterWriter{ - w: writer, - counter: &written, - }).Encode(message) + err = json.NewEncoder(writer).Encode(message) } } if err == nil { @@ -438,25 +449,49 @@ func (c *Client) writeInternal(message json.Marshaler) bool { } if sessionId := c.GetSessionId(); sessionId != "" { - c.logger.Printf("Could not send message %+v to client %s: %v", message, sessionId, err) + log.Printf("Could not send message %+v to client %s: %v", message, sessionId, err) } else { - c.logger.Printf("Could not send message %+v to %s: %v", message, c.RemoteAddr(), err) + log.Printf("Could not send message %+v to %s: %v", message, c.RemoteAddr(), err) } closeData = websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "") goto close } - - statsClientBytesTotal.WithLabelValues("outgoing").Add(float64(written)) - statsClientMessagesTotal.WithLabelValues("outgoing").Inc() return true close: c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil { if sessionId := c.GetSessionId(); sessionId != "" { - c.logger.Printf("Could not send close message to client %s: %v", sessionId, err) + log.Printf("Could not send close message to client %s: %v", sessionId, err) } else { - c.logger.Printf("Could not send close message to %s: %v", c.RemoteAddr(), err) + log.Printf("Could not send close message to %s: %v", c.RemoteAddr(), err) + } + } + return false +} + +func (c *Client) writeError(e error) bool { // nolint + message := &ServerMessage{ + Type: "error", + Error: NewError("internal_error", e.Error()), + } + c.mu.Lock() + defer c.mu.Unlock() + if c.conn == nil { + return false + } + + if !c.writeMessageLocked(message) { + return false + } + + closeData := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, e.Error()) + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint + if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil { + if sessionId := c.GetSessionId(); sessionId != "" { + log.Printf("Could not send close message to client %s: %v", sessionId, err) + } else { + log.Printf("Could not send close message to %s: %v", c.RemoteAddr(), err) } } return false @@ -472,19 +507,19 @@ func (c *Client) writeMessage(message WritableClientMessage) bool { return c.writeMessageLocked(message) } -// +checklocks:c.mu func (c *Client) writeMessageLocked(message WritableClientMessage) bool { if !c.writeInternal(message) { return false } - if message.CloseAfterSend(c) { - go func() { - if sc, ok := c.getHandler().(SessionCloserHandler); ok { - sc.CloseSession() - } - c.Close() - }() + session := c.GetSession() + if message.CloseAfterSend(session) { + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint + c.conn.WriteMessage(websocket.CloseMessage, []byte{}) // nolint + if session != nil { + go session.Close() + } + go c.Close() } return true @@ -502,14 +537,13 @@ func (c *Client) sendPing() bool { c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil { if sessionId := c.GetSessionId(); sessionId != "" { - c.logger.Printf("Could not send ping to client %s: %v", sessionId, err) + log.Printf("Could not send ping to client %s: %v", sessionId, err) } else { - c.logger.Printf("Could not send ping to %s: %v", c.RemoteAddr(), err) + log.Printf("Could not send ping to %s: %v", c.RemoteAddr(), err) } return false } - statsClientBytesTotal.WithLabelValues("outgoing").Add(float64(len(msg))) return true } diff --git a/client/client_test.go b/client/client_test.go deleted file mode 100644 index ef3aa2c..0000000 --- a/client/client_test.go +++ /dev/null @@ -1,339 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package client - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "net" - "net/http" - "net/http/httptest" - "slices" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" -) - -func TestCounterWriter(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - var b bytes.Buffer - var written int - w := &counterWriter{ - w: &b, - counter: &written, - } - if count, err := w.Write(nil); assert.NoError(err) && assert.Equal(0, count) { - assert.Equal(0, written) - } - if count, err := w.Write([]byte("foo")); assert.NoError(err) && assert.Equal(3, count) { - assert.Equal(3, written) - } -} - -type serverClient struct { - Client - - t *testing.T - handler *testHandler - - id string - received atomic.Uint32 - sessionClosed atomic.Bool -} - -func newTestClient(h *testHandler, r *http.Request, conn *websocket.Conn, id uint64) *serverClient { - result := &serverClient{ - t: h.t, - handler: h, - id: fmt.Sprintf("session-%d", id), - } - - addr := r.RemoteAddr - if host, _, err := net.SplitHostPort(addr); err == nil { - addr = host - } - - logger := logtest.NewLoggerForTest(h.t) - ctx := log.NewLoggerContext(r.Context(), logger) - result.SetConn(ctx, conn, addr, r.Header.Get("User-Agent"), false, result) - return result -} - -func (c *serverClient) WaitReceived(ctx context.Context, count uint32) error { - for { - if err := ctx.Err(); err != nil { - return err - } else if c.received.Load() >= count { - return nil - } - - time.Sleep(time.Millisecond) - } -} - -func (c *serverClient) GetSessionId() api.PublicSessionId { - return api.PublicSessionId(c.id) -} - -func (c *serverClient) OnClosed() { - c.Close() - c.handler.removeClient(c) -} - -func (c *serverClient) OnMessageReceived(message []byte) { - switch c.received.Add(1) { - case 1: - var s string - if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) { - assert.Equal(c.t, "Hello world!", s) - c.sendPing() - assert.EqualValues(c.t, "DE", c.Country()) - assert.False(c.t, c.Client.IsInRoom("room-id")) - c.SendMessage(&api.ServerMessage{ - Type: "welcome", - Welcome: &api.WelcomeServerMessage{ - Version: "1.0", - }, - }) - } - case 2: - var s string - if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) { - assert.Equal(c.t, "Send error", s) - c.SendError(api.NewError("test_error", "This is a test error.")) - } - case 3: - var s string - if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) { - assert.Equal(c.t, "Send bye", s) - c.SendByeResponseWithReason(nil, "Go away!") - } - } -} - -func (c *serverClient) OnRTTReceived(rtt time.Duration) { - -} - -func (c *serverClient) OnLookupCountry(addr string) geoip.Country { - return "DE" -} - -func (c *serverClient) IsInRoom(roomId string) bool { - return false -} - -func (c *serverClient) CloseSession() { - if c.sessionClosed.Swap(true) { - assert.Fail(c.t, "session closed more than once") - } -} - -type testHandler struct { - mu sync.Mutex - - t *testing.T - - upgrader websocket.Upgrader - - id atomic.Uint64 - // +checklocks:mu - activeClients map[string]*serverClient - // +checklocks:mu - allClients []*serverClient -} - -func newTestHandler(t *testing.T) *testHandler { - return &testHandler{ - t: t, - activeClients: make(map[string]*serverClient), - } -} - -func (h *testHandler) addClient(client *serverClient) { - h.mu.Lock() - defer h.mu.Unlock() - - h.activeClients[client.id] = client - h.allClients = append(h.allClients, client) -} - -func (h *testHandler) removeClient(client *serverClient) { - h.mu.Lock() - defer h.mu.Unlock() - - delete(h.activeClients, client.id) -} - -func (h *testHandler) getClients() []*serverClient { - h.mu.Lock() - defer h.mu.Unlock() - - return slices.Clone(h.allClients) -} - -func (h *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - conn, err := h.upgrader.Upgrade(w, r, nil) - if !assert.NoError(h.t, err) { - return - } - - id := h.id.Add(1) - client := newTestClient(h, r, conn, id) - h.addClient(client) - - closed := make(chan struct{}) - context.AfterFunc(client.Context(), func() { - close(closed) - }) - - go client.WritePump() - client.ReadPump() - <-closed -} - -type localClient struct { - t *testing.T - - conn *websocket.Conn -} - -func newLocalClient(t *testing.T, url string) *localClient { - t.Helper() - - conn, _, err := websocket.DefaultDialer.DialContext(t.Context(), url, nil) - require.NoError(t, err) - return &localClient{ - t: t, - - conn: conn, - } -} - -func (c *localClient) Close() error { - err := c.conn.Close() - if errors.Is(err, net.ErrClosed) { - err = nil - } - return err -} - -func (c *localClient) WriteJSON(v any) error { - return c.conn.WriteJSON(v) -} - -func (c *localClient) Write(v []byte) error { - return c.conn.WriteMessage(websocket.BinaryMessage, v) -} - -func (c *localClient) ReadJSON(v any) error { - return c.conn.ReadJSON(v) -} - -func TestClient(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - serverHandler := newTestHandler(t) - - server := httptest.NewServer(serverHandler) - t.Cleanup(func() { - server.Close() - }) - - client := newLocalClient(t, strings.ReplaceAll(server.URL, "http://", "ws://")) - t.Cleanup(func() { - assert.NoError(client.Close()) - }) - - var msg api.ServerMessage - - require.NoError(client.WriteJSON("Hello world!")) - if assert.NoError(client.ReadJSON(&msg)) && - assert.Equal("welcome", msg.Type) && - assert.NotNil(msg.Welcome) { - assert.Equal("1.0", msg.Welcome.Version) - } - if clients := serverHandler.getClients(); assert.Len(clients, 1) { - assert.False(clients[0].sessionClosed.Load()) - assert.EqualValues(1, clients[0].received.Load()) - } - - require.NoError(client.Write([]byte("Hello world!"))) - if assert.NoError(client.ReadJSON(&msg)) && - assert.Equal("error", msg.Type) && - assert.NotNil(msg.Error) { - assert.Equal("invalid_format", msg.Error.Code) - assert.Equal("Invalid data format.", msg.Error.Message) - } - - require.NoError(client.WriteJSON("Send error")) - if assert.NoError(client.ReadJSON(&msg)) && - assert.Equal("error", msg.Type) && - assert.NotNil(msg.Error) { - assert.Equal("test_error", msg.Error.Code) - assert.Equal("This is a test error.", msg.Error.Message) - } - if clients := serverHandler.getClients(); assert.Len(clients, 1) { - assert.False(clients[0].sessionClosed.Load()) - assert.EqualValues(2, clients[0].received.Load()) - } - - require.NoError(client.WriteJSON("Send bye")) - if assert.NoError(client.ReadJSON(&msg)) && - assert.Equal("bye", msg.Type) && - assert.NotNil(msg.Bye) { - assert.Equal("Go away!", msg.Bye.Reason) - } - if clients := serverHandler.getClients(); assert.Len(clients, 1) { - assert.EqualValues(3, clients[0].received.Load()) - } - - // Sending a "bye" will close the connection. - var we *websocket.CloseError - if err := client.ReadJSON(&msg); assert.ErrorAs(err, &we) { - assert.Equal(websocket.CloseNormalClosure, we.Code) - assert.Empty(we.Text) - } - if clients := serverHandler.getClients(); assert.Len(clients, 1) { - assert.True(clients[0].sessionClosed.Load()) - assert.EqualValues(3, clients[0].received.Load()) - } -} diff --git a/client/ip.go b/client/ip.go deleted file mode 100644 index 5f73195..0000000 --- a/client/ip.go +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package client - -import ( - "net" - "net/http" - "slices" - "strings" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" -) - -var ( - DefaultTrustedProxies = container.DefaultPrivateIPs() -) - -func GetRealUserIP(r *http.Request, trusted *container.IPList) string { - addr := r.RemoteAddr - if host, _, err := net.SplitHostPort(addr); err == nil { - addr = host - } - - ip := net.ParseIP(addr) - if len(ip) == 0 { - return addr - } - - // Don't check any headers if the server can be reached by untrusted clients directly. - if trusted == nil || !trusted.Contains(ip) { - return addr - } - - if realIP := r.Header.Get("X-Real-IP"); realIP != "" { - if ip := net.ParseIP(realIP); len(ip) > 0 { - return realIP - } - } - - // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address - forwarded := strings.Split(strings.Join(r.Header.Values("X-Forwarded-For"), ","), ",") - if len(forwarded) > 0 { - slices.Reverse(forwarded) - var lastTrusted string - for _, hop := range forwarded { - hop = strings.TrimSpace(hop) - // Make sure to remove any port. - if host, _, err := net.SplitHostPort(hop); err == nil { - hop = host - } - - ip := net.ParseIP(hop) - if len(ip) == 0 { - continue - } - - if trusted.Contains(ip) { - lastTrusted = hop - continue - } - - return hop - } - - // If all entries in the "X-Forwarded-For" list are trusted, the left-most - // will be the client IP. This can happen if a subnet is trusted and the - // client also has an IP from this subnet. - if lastTrusted != "" { - return lastTrusted - } - } - - return addr -} diff --git a/client/ip_test.go b/client/ip_test.go deleted file mode 100644 index fbe5358..0000000 --- a/client/ip_test.go +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package client - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" -) - -func TestGetRealUserIP(t *testing.T) { - t.Parallel() - testcases := []struct { - expected string - headers http.Header - trusted string - addr string - }{ - { - "192.168.1.2", - nil, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "invalid-ip", - nil, - "192.168.0.0/16", - "invalid-ip", - }, - { - "invalid-ip", - nil, - "192.168.0.0/16", - "invalid-ip:12345", - }, - { - "10.11.12.13", - nil, - "192.168.0.0/16", - "10.11.12.13:23456", - }, - { - "10.11.12.13", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "2002:db8::1", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"2002:db8::1"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "11.12.13.14", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "11.12.13.14", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14:1234, 192.168.30.32:2345"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "10.11.12.13", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"}, - }, - "2001:db8::/48", - "[2001:db8::1]:23456", - }, - { - "2002:db8::1", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"2002:db8::1"}, - }, - "2001:db8::/48", - "[2001:db8::1]:23456", - }, - { - "2002:db8::1", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 192.168.30.32"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "2002:db8::1", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 2001:db8::1"}, - }, - "192.168.0.0/16, 2001:db8::/48", - "192.168.1.2:23456", - }, - { - "2002:db8::1", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 192.168.30.32"}, - }, - "192.168.0.0/16, 2001:db8::/48", - "[2001:db8::1]:23456", - }, - { - "2002:db8::1", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 2001:db8::2"}, - }, - "2001:db8::/48", - "[2001:db8::1]:23456", - }, - // "X-Real-IP" has preference before "X-Forwarded-For" - { - "10.11.12.13", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"}, - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - // Multiple "X-Forwarded-For" headers are merged. - { - "11.12.13.14", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14", "192.168.30.32"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "11.12.13.14", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"1.2.3.4", "11.12.13.14", "192.168.30.32"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "11.12.13.14", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"1.2.3.4", "2.3.4.5", "11.12.13.14", "192.168.31.32", "192.168.30.32"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - // Headers are ignored if coming from untrusted clients. - { - "10.11.12.13", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"11.12.13.14"}, - }, - "192.168.0.0/16", - "10.11.12.13:23456", - }, - { - "10.11.12.13", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"}, - }, - "192.168.0.0/16", - "10.11.12.13:23456", - }, - // X-Forwarded-For is filtered for trusted proxies. - { - "1.2.3.4", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "1.2.3.4", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4, 192.168.2.3"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "10.11.12.13", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4"}, - }, - "192.168.0.0/16", - "10.11.12.13:23456", - }, - // Invalid IPs are ignored. - { - "192.168.1.2", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "11.12.13.14", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"}, - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "11.12.13.14", - http.Header{ - http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"}, - http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32, proxy1"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "192.168.1.2", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"this-is-not-an-ip"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - { - "192.168.2.3", - http.Header{ - http.CanonicalHeaderKey("x-forwarded-for"): []string{"this-is-not-an-ip, 192.168.2.3"}, - }, - "192.168.0.0/16", - "192.168.1.2:23456", - }, - } - - for _, tc := range testcases { - trustedProxies, err := container.ParseIPList(tc.trusted) - if !assert.NoError(t, err, "invalid trusted proxies in %+v", tc) { - continue - } - request := &http.Request{ - RemoteAddr: tc.addr, - Header: tc.headers, - } - assert.Equal(t, tc.expected, GetRealUserIP(request, trustedProxies), "failed for %+v", tc) - } -} diff --git a/cmd/client/main.go b/client/main.go similarity index 67% rename from cmd/client/main.go rename to client/main.go index f04d3db..d66503f 100644 --- a/cmd/client/main.go +++ b/client/main.go @@ -35,7 +35,7 @@ import ( "os" "os/signal" "runtime" - "runtime/pprof" + "strings" "sync" "sync/atomic" "time" @@ -44,27 +44,14 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/mailru/easyjson" - "github.com/mailru/easyjson/jlexer" - "github.com/mailru/easyjson/jwriter" - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) var ( - version = "unreleased" - - showVersion = flag.Bool("version", false, "show version and quit") - addr = flag.String("addr", "localhost:28080", "http service address") - configFlag = flag.String("config", "server.conf", "config file to use") - - cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") - - memprofile = flag.String("memprofile", "", "write memory profile to file") + config = flag.String("config", "server.conf", "config file to use") maxClients = flag.Int("maxClients", 100, "number of client connections") @@ -88,47 +75,47 @@ const ( maxMessageSize = 64 * 1024 ) +type Stats struct { + numRecvMessages atomic.Uint64 + numSentMessages atomic.Uint64 + resetRecvMessages uint64 + resetSentMessages uint64 + + start time.Time +} + +func (s *Stats) reset(start time.Time) { + s.resetRecvMessages = s.numRecvMessages.Load() + s.resetSentMessages = s.numSentMessages.Load() + s.start = start +} + +func (s *Stats) Log() { + now := time.Now() + duration := now.Sub(s.start) + perSec := uint64(duration / time.Second) + if perSec == 0 { + return + } + + totalSentMessages := s.numSentMessages.Load() + sentMessages := totalSentMessages - s.resetSentMessages + totalRecvMessages := s.numRecvMessages.Load() + recvMessages := totalRecvMessages - s.resetRecvMessages + log.Printf("Stats: sent=%d (%d/sec), recv=%d (%d/sec), delta=%d", + totalSentMessages, sentMessages/perSec, + totalRecvMessages, recvMessages/perSec, + totalSentMessages-totalRecvMessages) + s.reset(now) +} + type MessagePayload struct { Now time.Time `json:"now"` } -func (m *MessagePayload) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - w.RawByte('{') - w.RawString("\"now\":") - w.Raw(m.Now.MarshalJSON()) - w.RawByte('}') - return w.Buffer.BuildBytes(), w.Error -} - -func (m *MessagePayload) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - r.Delim('{') - for !r.IsDelim('}') { - key := r.UnsafeFieldName(false) - r.WantColon() - switch key { - case "now": - if r.IsNull() { - r.Skip() - } else { - if data := r.Raw(); r.Ok() { - r.AddError((m.Now).UnmarshalJSON(data)) - } - } - default: - r.SkipRecursive() - } - r.WantComma() - } - r.Delim('}') - r.Consumed() - - return r.Error() -} - type SignalingClient struct { - readyWg *sync.WaitGroup // +checklocksignore: Only written to from constructor. + readyWg *sync.WaitGroup + cookie *signaling.SessionIdCodec conn *websocket.Conn @@ -137,16 +124,13 @@ type SignalingClient struct { stopChan chan struct{} - lock sync.Mutex - // +checklocks:lock - privateSessionId api.PrivateSessionId - // +checklocks:lock - publicSessionId api.PublicSessionId - // +checklocks:lock - userId string + lock sync.Mutex + privateSessionId string + publicSessionId string + userId string } -func NewSignalingClient(url string, stats *Stats, readyWg *sync.WaitGroup, doneWg *sync.WaitGroup) (*SignalingClient, error) { +func NewSignalingClient(cookie *signaling.SessionIdCodec, url string, stats *Stats, readyWg *sync.WaitGroup, doneWg *sync.WaitGroup) (*SignalingClient, error) { conn, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { return nil, err @@ -154,6 +138,7 @@ func NewSignalingClient(url string, stats *Stats, readyWg *sync.WaitGroup, doneW client := &SignalingClient{ readyWg: readyWg, + cookie: cookie, conn: conn, @@ -161,8 +146,15 @@ func NewSignalingClient(url string, stats *Stats, readyWg *sync.WaitGroup, doneW stopChan: make(chan struct{}), } - doneWg.Go(client.readPump) - doneWg.Go(client.writePump) + doneWg.Add(2) + go func() { + defer doneWg.Done() + client.readPump() + }() + go func() { + defer doneWg.Done() + client.writePump() + }() return client, nil } @@ -177,10 +169,6 @@ func (c *SignalingClient) Close() { c.lock.Lock() c.publicSessionId = "" c.privateSessionId = "" - c.writeInternal(&api.ClientMessage{ - Type: "bye", - Bye: &api.ByeClientMessage{}, - }) c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) // nolint c.conn.Close() @@ -188,7 +176,7 @@ func (c *SignalingClient) Close() { c.lock.Unlock() } -func (c *SignalingClient) Send(message *api.ClientMessage) { +func (c *SignalingClient) Send(message *signaling.ClientMessage) { c.lock.Lock() if c.conn == nil { c.lock.Unlock() @@ -203,11 +191,9 @@ func (c *SignalingClient) Send(message *api.ClientMessage) { c.lock.Unlock() } -func (c *SignalingClient) processMessage(message *api.ServerMessage) { +func (c *SignalingClient) processMessage(message *signaling.ServerMessage) { c.stats.numRecvMessages.Add(1) switch message.Type { - case "welcome": - // Ignore welcome message. case "hello": c.processHelloMessage(message) case "message": @@ -223,25 +209,37 @@ func (c *SignalingClient) processMessage(message *api.ServerMessage) { } } -func (c *SignalingClient) processHelloMessage(message *api.ServerMessage) { +func (c *SignalingClient) privateToPublicSessionId(privateId string) string { + data, err := c.cookie.DecodePrivate(privateId) + if err != nil { + panic(fmt.Sprintf("could not decode private session id: %s", err)) + } + publicId, err := c.cookie.EncodePublic(data) + if err != nil { + panic(fmt.Sprintf("could not encode public id: %s", err)) + } + return publicId +} + +func (c *SignalingClient) processHelloMessage(message *signaling.ServerMessage) { c.lock.Lock() defer c.lock.Unlock() c.privateSessionId = message.Hello.ResumeId - c.publicSessionId = message.Hello.SessionId + c.publicSessionId = c.privateToPublicSessionId(c.privateSessionId) c.userId = message.Hello.UserId log.Printf("Registered as %s (userid %s)", c.privateSessionId, c.userId) c.readyWg.Done() } -func (c *SignalingClient) PublicSessionId() api.PublicSessionId { +func (c *SignalingClient) PublicSessionId() string { c.lock.Lock() defer c.lock.Unlock() return c.publicSessionId } -func (c *SignalingClient) processMessageMessage(message *api.ServerMessage) { +func (c *SignalingClient) processMessageMessage(message *signaling.ServerMessage) { var msg MessagePayload - if err := msg.UnmarshalJSON(message.Message.Data); err != nil { + if err := json.Unmarshal(message.Message.Data, &msg); err != nil { log.Println("Error in unmarshal", err) return } @@ -296,9 +294,7 @@ func (c *SignalingClient) readPump() { break } - c.stats.numRecvBytes.Add(uint64(decodeBuffer.Len())) - - var message api.ServerMessage + var message signaling.ServerMessage if err := message.UnmarshalJSON(decodeBuffer.Bytes()); err != nil { log.Printf("Error: %v", err) break @@ -308,14 +304,13 @@ func (c *SignalingClient) readPump() { } } -func (c *SignalingClient) writeInternal(message *api.ClientMessage) bool { +func (c *SignalingClient) writeInternal(message *signaling.ClientMessage) bool { var closeData []byte c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint - var written int writer, err := c.conn.NextWriter(websocket.TextMessage) if err == nil { - written, err = easyjson.MarshalToWriter(message, writer) + _, err = easyjson.MarshalToWriter(message, writer) } if err != nil { if err == websocket.ErrCloseSent { @@ -331,9 +326,6 @@ func (c *SignalingClient) writeInternal(message *api.ClientMessage) bool { writer.Close() c.stats.numSentMessages.Add(1) - if written > 0 { - c.stats.numSentBytes.Add(uint64(written)) - } return true close: @@ -377,7 +369,7 @@ func (c *SignalingClient) writePump() { } func (c *SignalingClient) SendMessages(clients []*SignalingClient) { - sessionIds := make(map[*SignalingClient]api.PublicSessionId) + sessionIds := make(map[*SignalingClient]string) for _, c := range clients { sessionIds[c] = c.PublicSessionId() } @@ -395,11 +387,11 @@ func (c *SignalingClient) SendMessages(clients []*SignalingClient) { msgdata := MessagePayload{ Now: now, } - data, _ := msgdata.MarshalJSON() - msg := &api.ClientMessage{ + data, _ := json.Marshal(msgdata) + msg := &signaling.ClientMessage{ Type: "message", - Message: &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ + Message: &signaling.MessageClientMessage{ + Recipient: signaling.MessageClientMessageRecipient{ Type: "session", SessionId: sessionIds[recipient], }, @@ -413,35 +405,35 @@ func (c *SignalingClient) SendMessages(clients []*SignalingClient) { } func registerAuthHandler(router *mux.Router) { - router.HandleFunc("/ocs/v2.php/apps/spreed/api/v1/signaling/backend", func(w http.ResponseWriter, r *http.Request) { + router.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { log.Println("Error reading body:", err) return } - rnd := r.Header.Get(talk.HeaderBackendSignalingRandom) - checksum := r.Header.Get(talk.HeaderBackendSignalingChecksum) + rnd := r.Header.Get(signaling.HeaderBackendSignalingRandom) + checksum := r.Header.Get(signaling.HeaderBackendSignalingChecksum) if rnd == "" || checksum == "" { log.Println("No checksum headers found") return } - if verify := talk.CalculateBackendChecksum(rnd, body, backendSecret); verify != checksum { + if verify := signaling.CalculateBackendChecksum(rnd, body, backendSecret); verify != checksum { log.Println("Backend checksum verification failed") return } - var request talk.BackendClientRequest + var request signaling.BackendClientRequest if err := request.UnmarshalJSON(body); err != nil { log.Println(err) return } - response := &talk.BackendClientResponse{ + response := &signaling.BackendClientResponse{ Type: "auth", - Auth: &talk.BackendClientAuthResponse{ - Version: talk.BackendVersion, + Auth: &signaling.BackendClientAuthResponse{ + Version: signaling.BackendVersion, UserId: "sample-user", }, } @@ -453,9 +445,9 @@ func registerAuthHandler(router *mux.Router) { } rawdata := json.RawMessage(data) - payload := &talk.OcsResponse{ - Ocs: &talk.OcsBody{ - Meta: talk.OcsMeta{ + payload := &signaling.OcsResponse{ + Ocs: &signaling.OcsBody{ + Meta: signaling.OcsMeta{ Status: "ok", StatusCode: http.StatusOK, Message: http.StatusText(http.StatusOK), @@ -496,48 +488,38 @@ func main() { flag.Parse() log.SetFlags(0) - if *showVersion { - fmt.Printf("nextcloud-spreed-signaling-client version %s/%s\n", version, runtime.Version()) - os.Exit(0) - } - - cfg, err := goconf.ReadConfigFile(*configFlag) + config, err := goconf.ReadConfigFile(*config) if err != nil { log.Fatal("Could not read configuration: ", err) } - secret, _ := config.GetStringOptionWithEnv(cfg, "backend", "secret") + secret, _ := config.GetString("backend", "secret") backendSecret = []byte(secret) - log.Printf("Using a maximum of %d CPUs", runtime.GOMAXPROCS(0)) - - if *cpuprofile != "" { - f, err := os.Create(*cpuprofile) - if err != nil { - log.Fatal(err) - } - - if err := pprof.StartCPUProfile(f); err != nil { - log.Fatalf("Error writing CPU profile to %s: %s", *cpuprofile, err) - } - log.Printf("Writing CPU profile to %s ...", *cpuprofile) - defer pprof.StopCPUProfile() + hashKey, _ := config.GetString("sessions", "hashkey") + switch len(hashKey) { + case 32: + case 64: + default: + log.Printf("WARNING: The sessions hash key should be 32 or 64 bytes but is %d bytes", len(hashKey)) } - if *memprofile != "" { - f, err := os.Create(*memprofile) - if err != nil { - log.Fatal(err) // nolint (defer pprof.StopCPUProfile() will not run which is ok in case of errors) - } - - defer func() { - log.Printf("Writing Memory profile to %s ...", *memprofile) - runtime.GC() - if err := pprof.WriteHeapProfile(f); err != nil { - log.Printf("Error writing Memory profile to %s: %s", *memprofile, err) - } - }() + blockKey, _ := config.GetString("sessions", "blockkey") + blockBytes := []byte(blockKey) + switch len(blockKey) { + case 0: + blockBytes = nil + case 16: + case 24: + case 32: + default: + log.Fatalf("The sessions block key must be 16, 24 or 32 bytes but is %d bytes", len(blockKey)) } + cookie := signaling.NewSessionIdCodec([]byte(hashKey), blockBytes) + + cpus := runtime.NumCPU() + runtime.GOMAXPROCS(cpus) + log.Printf("Using a maximum of %d CPUs", cpus) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) @@ -562,7 +544,7 @@ func main() { urls := make([]url.URL, 0) urlstrings := make([]string, 0) - for host := range internal.SplitEntries(*addr, ",") { + for _, host := range strings.Split(*addr, ",") { u := url.URL{ Scheme: "ws", Host: host, @@ -586,19 +568,19 @@ func main() { var readyWg sync.WaitGroup for i := 0; i < *maxClients; i++ { - client, err := NewSignalingClient(urls[i%len(urls)].String(), stats, &readyWg, &doneWg) + client, err := NewSignalingClient(cookie, urls[i%len(urls)].String(), stats, &readyWg, &doneWg) if err != nil { log.Fatal(err) } defer client.Close() readyWg.Add(1) - request := &api.ClientMessage{ + request := &signaling.ClientMessage{ Type: "hello", - Hello: &api.HelloClientMessage{ - Version: api.HelloVersionV1, - Auth: &api.HelloClientMessageAuth{ - Url: backendUrl, + Hello: &signaling.HelloClientMessage{ + Version: signaling.HelloVersionV1, + Auth: &signaling.HelloClientMessageAuth{ + Url: backendUrl + "/auth", Params: json.RawMessage("{}"), }, }, @@ -614,9 +596,11 @@ func main() { log.Println("All connections established") for _, c := range clients { - doneWg.Go(func() { + doneWg.Add(1) + go func(c *SignalingClient) { + defer doneWg.Done() c.SendMessages(clients) - }) + }(c) } stats.start = time.Now() diff --git a/client/stats_prometheus.go b/client/stats_prometheus.go deleted file mode 100644 index 2d263b3..0000000 --- a/client/stats_prometheus.go +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package client - -import ( - "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" -) - -var ( - statsClientRTT = prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: "signaling", - Subsystem: "client", - Name: "rtt", - Help: "The roundtrip time of WebSocket ping messages in milliseconds", - Buckets: prometheus.ExponentialBucketsRange(1, 30000, 50), - }) - statsClientBytesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "client", - Name: "bytes_total", - Help: "The total number of bytes sent to or received by clients", - }, []string{"direction"}) - statsClientMessagesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "client", - Name: "messages_total", - Help: "The total number of messages sent to or received by clients", - }, []string{"direction"}) - - clientStats = []prometheus.Collector{ - statsClientRTT, - statsClientBytesTotal, - statsClientMessagesTotal, - } -) - -func RegisterClientStats() { - metrics.RegisterAll(clientStats...) -} diff --git a/server/hub_client_stats_prometheus.go b/client_stats_prometheus.go similarity index 91% rename from server/hub_client_stats_prometheus.go rename to client_stats_prometheus.go index 518548c..e20447e 100644 --- a/server/hub_client_stats_prometheus.go +++ b/client_stats_prometheus.go @@ -19,12 +19,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" ) var ( @@ -41,5 +39,5 @@ var ( ) func RegisterClientStats() { - metrics.RegisterAll(clientStats...) + registerAll(clientStats...) } diff --git a/clientsession.go b/clientsession.go new file mode 100644 index 0000000..c59920a --- /dev/null +++ b/clientsession.go @@ -0,0 +1,1500 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/url" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/pion/sdp/v3" +) + +var ( + // Warn if a session has 32 or more pending messages. + warnPendingMessagesCount = 32 + + PathToOcsSignalingBackend = "ocs/v2.php/apps/spreed/api/v1/signaling/backend" +) + +const ( + FederatedRoomSessionIdPrefix = "federated|" +) + +// ResponseHandlerFunc will return "true" has been fully processed. +type ResponseHandlerFunc func(message *ClientMessage) bool + +type ClientSession struct { + hub *Hub + events AsyncEvents + privateId string + publicId string + data *SessionIdData + ctx context.Context + closeFunc context.CancelFunc + + clientType string + features []string + userId string + userData json.RawMessage + + parseUserData func() (map[string]interface{}, error) + + inCall Flags + supportsPermissions bool + permissions map[Permission]bool + + backend *Backend + backendUrl string + parsedBackendUrl *url.URL + + mu sync.Mutex + + client HandlerClient + room atomic.Pointer[Room] + roomJoinTime atomic.Int64 + federation atomic.Pointer[FederationClient] + + roomSessionIdLock sync.RWMutex + roomSessionId string + + publisherWaiters ChannelWaiters + + publishers map[StreamType]McuPublisher + subscribers map[string]McuSubscriber + + pendingClientMessages []*ServerMessage + hasPendingChat bool + hasPendingParticipantsUpdate bool + + virtualSessions map[*VirtualSession]bool + + seenJoinedLock sync.Mutex + seenJoinedEvents map[string]bool + + responseHandlersLock sync.Mutex + responseHandlers map[string]ResponseHandlerFunc +} + +func NewClientSession(hub *Hub, privateId string, publicId string, data *SessionIdData, backend *Backend, hello *HelloClientMessage, auth *BackendClientAuthResponse) (*ClientSession, error) { + ctx, closeFunc := context.WithCancel(context.Background()) + s := &ClientSession{ + hub: hub, + events: hub.events, + privateId: privateId, + publicId: publicId, + data: data, + ctx: ctx, + closeFunc: closeFunc, + + clientType: hello.Auth.Type, + features: hello.Features, + userId: auth.UserId, + userData: auth.User, + parseUserData: parseUserData(auth.User), + + backend: backend, + } + if s.clientType == HelloClientTypeInternal { + s.backendUrl = hello.Auth.internalParams.Backend + s.parsedBackendUrl = hello.Auth.internalParams.parsedBackend + if !s.HasFeature(ClientFeatureInternalInCall) { + s.SetInCall(FlagInCall | FlagWithAudio) + } + } else { + s.backendUrl = hello.Auth.Url + s.parsedBackendUrl = hello.Auth.parsedUrl + } + if !strings.Contains(s.backendUrl, "/ocs/v2.php/") { + backendUrl := s.backendUrl + if !strings.HasSuffix(backendUrl, "/") { + backendUrl += "/" + } + backendUrl += PathToOcsSignalingBackend + u, err := url.Parse(backendUrl) + if err != nil { + return nil, err + } + + if strings.Contains(u.Host, ":") && hasStandardPort(u) { + u.Host = u.Hostname() + } + + s.backendUrl = backendUrl + s.parsedBackendUrl = u + } + + if err := s.SubscribeEvents(); err != nil { + return nil, err + } + return s, nil +} + +func (s *ClientSession) Context() context.Context { + return s.ctx +} + +func (s *ClientSession) PrivateId() string { + return s.privateId +} + +func (s *ClientSession) PublicId() string { + return s.publicId +} + +func (s *ClientSession) RoomSessionId() string { + s.roomSessionIdLock.RLock() + defer s.roomSessionIdLock.RUnlock() + return s.roomSessionId +} + +func (s *ClientSession) Data() *SessionIdData { + return s.data +} + +func (s *ClientSession) ClientType() string { + return s.clientType +} + +// GetInCall is only used for internal clients. +func (s *ClientSession) GetInCall() int { + return int(s.inCall.Get()) +} + +func (s *ClientSession) SetInCall(inCall int) bool { + if inCall < 0 { + inCall = 0 + } + + return s.inCall.Set(uint32(inCall)) +} + +func (s *ClientSession) GetFeatures() []string { + return s.features +} + +func (s *ClientSession) HasFeature(feature string) bool { + for _, f := range s.features { + if f == feature { + return true + } + } + return false +} + +// HasPermission checks if the session has the passed permissions. +func (s *ClientSession) HasPermission(permission Permission) bool { + s.mu.Lock() + defer s.mu.Unlock() + + return s.hasPermissionLocked(permission) +} + +// HasAnyPermission checks if the session has one of the passed permissions. +func (s *ClientSession) HasAnyPermission(permission ...Permission) bool { + if len(permission) == 0 { + return false + } + + s.mu.Lock() + defer s.mu.Unlock() + + return s.hasAnyPermissionLocked(permission...) +} + +func (s *ClientSession) hasAnyPermissionLocked(permission ...Permission) bool { + if len(permission) == 0 { + return false + } + + for _, p := range permission { + if s.hasPermissionLocked(p) { + return true + } + } + return false +} + +func (s *ClientSession) hasPermissionLocked(permission Permission) bool { + if !s.supportsPermissions { + // Old-style session that doesn't receive permissions from Nextcloud. + if result, found := DefaultPermissionOverrides[permission]; found { + return result + } + return true + } + + if val, found := s.permissions[permission]; found { + return val + } + return false +} + +func permissionsEqual(a, b map[Permission]bool) bool { + if a == nil && b == nil { + return true + } else if a != nil && b == nil { + return false + } else if a == nil && b != nil { + return false + } + if len(a) != len(b) { + return false + } + + for k, v1 := range a { + if v2, found := b[k]; !found || v1 != v2 { + return false + } + } + return true +} + +func (s *ClientSession) SetPermissions(permissions []Permission) { + var p map[Permission]bool + for _, permission := range permissions { + if p == nil { + p = make(map[Permission]bool) + } + p[permission] = true + } + + s.mu.Lock() + defer s.mu.Unlock() + if s.supportsPermissions && permissionsEqual(s.permissions, p) { + return + } + + s.permissions = p + s.supportsPermissions = true + log.Printf("Permissions of session %s changed: %s", s.PublicId(), permissions) +} + +func (s *ClientSession) Backend() *Backend { + return s.backend +} + +func (s *ClientSession) BackendUrl() string { + return s.backendUrl +} + +func (s *ClientSession) ParsedBackendUrl() *url.URL { + return s.parsedBackendUrl +} + +func (s *ClientSession) AuthUserId() string { + return s.userId +} + +func (s *ClientSession) UserId() string { + userId := s.userId + if userId == "" { + if room := s.GetRoom(); room != nil { + if data := room.GetRoomSessionData(s); data != nil { + userId = data.UserId + } + } + } + return userId +} + +func (s *ClientSession) UserData() json.RawMessage { + return s.userData +} + +func (s *ClientSession) ParsedUserData() (map[string]interface{}, error) { + return s.parseUserData() +} + +func (s *ClientSession) SetRoom(room *Room) { + s.room.Store(room) + s.onRoomSet(room != nil) +} + +func (s *ClientSession) onRoomSet(hasRoom bool) { + if hasRoom { + s.roomJoinTime.Store(time.Now().UnixNano()) + } else { + s.roomJoinTime.Store(0) + } + + s.seenJoinedLock.Lock() + defer s.seenJoinedLock.Unlock() + s.seenJoinedEvents = nil +} + +func (s *ClientSession) GetFederationClient() *FederationClient { + return s.federation.Load() +} + +func (s *ClientSession) SetFederationClient(federation *FederationClient) { + s.mu.Lock() + defer s.mu.Unlock() + + s.doLeaveRoom(true) + s.onRoomSet(federation != nil) + + if prev := s.federation.Swap(federation); prev != nil && prev != federation { + prev.Close() + } +} + +func (s *ClientSession) GetRoom() *Room { + return s.room.Load() +} + +func (s *ClientSession) getRoomJoinTime() time.Time { + t := s.roomJoinTime.Load() + if t == 0 { + return time.Time{} + } + + return time.Unix(0, t) +} + +func (s *ClientSession) releaseMcuObjects() { + if len(s.publishers) > 0 { + go func(publishers map[StreamType]McuPublisher) { + ctx := context.Background() + for _, publisher := range publishers { + publisher.Close(ctx) + } + }(s.publishers) + s.publishers = nil + } + if len(s.subscribers) > 0 { + go func(subscribers map[string]McuSubscriber) { + ctx := context.Background() + for _, subscriber := range subscribers { + subscriber.Close(ctx) + } + }(s.subscribers) + s.subscribers = nil + } +} + +func (s *ClientSession) Close() { + s.closeAndWait(true) +} + +func (s *ClientSession) closeAndWait(wait bool) { + s.closeFunc() + s.hub.removeSession(s) + + if prev := s.federation.Swap(nil); prev != nil { + prev.Close() + } + + s.mu.Lock() + defer s.mu.Unlock() + if s.userId != "" { + s.events.UnregisterUserListener(s.userId, s.backend, s) + } + s.events.UnregisterSessionListener(s.publicId, s.backend, s) + go func(virtualSessions map[*VirtualSession]bool) { + for session := range virtualSessions { + session.Close() + } + }(s.virtualSessions) + s.virtualSessions = nil + s.releaseMcuObjects() + s.clearClientLocked(nil) + s.backend.RemoveSession(s) +} + +func (s *ClientSession) SubscribeEvents() error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.userId != "" { + if err := s.events.RegisterUserListener(s.userId, s.backend, s); err != nil { + return err + } + } + + return s.events.RegisterSessionListener(s.publicId, s.backend, s) +} + +func (s *ClientSession) UpdateRoomSessionId(roomSessionId string) error { + s.roomSessionIdLock.Lock() + defer s.roomSessionIdLock.Unlock() + + if s.roomSessionId == roomSessionId { + return nil + } + + if err := s.hub.roomSessions.SetRoomSession(s, roomSessionId); err != nil { + return err + } + + if roomSessionId != "" { + if room := s.GetRoom(); room != nil { + log.Printf("Session %s updated room session id to %s in room %s", s.PublicId(), roomSessionId, room.Id()) + } else if client := s.GetFederationClient(); client != nil { + log.Printf("Session %s updated room session id to %s in federated room %s", s.PublicId(), roomSessionId, client.RemoteRoomId()) + } else { + log.Printf("Session %s updated room session id to %s in unknown room", s.PublicId(), roomSessionId) + } + } else { + if room := s.GetRoom(); room != nil { + log.Printf("Session %s cleared room session id in room %s", s.PublicId(), room.Id()) + } else if client := s.GetFederationClient(); client != nil { + log.Printf("Session %s cleared room session id in federated room %s", s.PublicId(), client.RemoteRoomId()) + } else { + log.Printf("Session %s cleared room session id in unknown room", s.PublicId()) + } + } + + s.roomSessionId = roomSessionId + return nil +} + +func (s *ClientSession) SubscribeRoomEvents(roomid string, roomSessionId string) error { + s.roomSessionIdLock.Lock() + defer s.roomSessionIdLock.Unlock() + + if err := s.events.RegisterRoomListener(roomid, s.backend, s); err != nil { + return err + } + + if roomSessionId != "" { + if err := s.hub.roomSessions.SetRoomSession(s, roomSessionId); err != nil { + s.doUnsubscribeRoomEvents(true) + return err + } + } + log.Printf("Session %s joined room %s with room session id %s", s.PublicId(), roomid, roomSessionId) + s.roomSessionId = roomSessionId + return nil +} + +func (s *ClientSession) LeaveCall() { + s.mu.Lock() + defer s.mu.Unlock() + + room := s.GetRoom() + if room == nil { + return + } + + log.Printf("Session %s left call %s", s.PublicId(), room.Id()) + s.releaseMcuObjects() +} + +func (s *ClientSession) LeaveRoom(notify bool) *Room { + return s.LeaveRoomWithMessage(notify, nil) +} + +func (s *ClientSession) LeaveRoomWithMessage(notify bool, message *ClientMessage) *Room { + if prev := s.federation.Swap(nil); prev != nil { + // Session was connected to a federation room. + if err := prev.Leave(message); err != nil { + log.Printf("Error leaving room for session %s on federation client %s: %s", s.PublicId(), prev.URL(), err) + prev.Close() + } + return nil + } + + s.mu.Lock() + defer s.mu.Unlock() + + return s.doLeaveRoom(notify) +} + +func (s *ClientSession) doLeaveRoom(notify bool) *Room { + room := s.GetRoom() + if room == nil { + return nil + } + + s.doUnsubscribeRoomEvents(notify) + s.SetRoom(nil) + s.releaseMcuObjects() + room.RemoveSession(s) + return room +} + +func (s *ClientSession) UnsubscribeRoomEvents() { + s.mu.Lock() + defer s.mu.Unlock() + + s.doUnsubscribeRoomEvents(true) +} + +func (s *ClientSession) doUnsubscribeRoomEvents(notify bool) { + room := s.GetRoom() + if room != nil { + s.events.UnregisterRoomListener(room.Id(), s.Backend(), s) + } + s.hub.roomSessions.DeleteRoomSession(s) + + s.roomSessionIdLock.Lock() + defer s.roomSessionIdLock.Unlock() + if notify && room != nil && s.roomSessionId != "" && !strings.HasPrefix(s.roomSessionId, FederatedRoomSessionIdPrefix) { + // Notify + go func(sid string) { + ctx := context.Background() + request := NewBackendClientRoomRequest(room.Id(), s.userId, sid) + request.Room.UpdateFromSession(s) + request.Room.Action = "leave" + var response map[string]interface{} + if err := s.hub.backend.PerformJSONRequest(ctx, s.ParsedBackendUrl(), request, &response); err != nil { + log.Printf("Could not notify about room session %s left room %s: %s", sid, room.Id(), err) + } else { + log.Printf("Removed room session %s: %+v", sid, response) + } + }(s.roomSessionId) + } + s.roomSessionId = "" +} + +func (s *ClientSession) ClearClient(client HandlerClient) { + s.mu.Lock() + defer s.mu.Unlock() + + s.clearClientLocked(client) +} + +func (s *ClientSession) clearClientLocked(client HandlerClient) { + if s.client == nil { + return + } else if client != nil && s.client != client { + log.Printf("Trying to clear other client in session %s", s.PublicId()) + return + } + + prevClient := s.client + s.client = nil + prevClient.SetSession(nil) +} + +func (s *ClientSession) GetClient() HandlerClient { + s.mu.Lock() + defer s.mu.Unlock() + + return s.getClientUnlocked() +} + +func (s *ClientSession) getClientUnlocked() HandlerClient { + return s.client +} + +func (s *ClientSession) SetClient(client HandlerClient) HandlerClient { + if client == nil { + panic("Use ClearClient to set the client to nil") + } + + s.mu.Lock() + defer s.mu.Unlock() + + if client == s.client { + // No change + return nil + } + + client.SetSession(s) + prev := s.client + if prev != nil { + s.clearClientLocked(prev) + } + s.client = client + return prev +} + +func (s *ClientSession) sendOffer(client McuClient, sender string, streamType StreamType, offer map[string]interface{}) { + offer_message := &AnswerOfferMessage{ + To: s.PublicId(), + From: sender, + Type: "offer", + RoomType: string(streamType), + Payload: offer, + Sid: client.Sid(), + } + offer_data, err := json.Marshal(offer_message) + if err != nil { + log.Println("Could not serialize offer", offer_message, err) + return + } + response_message := &ServerMessage{ + Type: "message", + Message: &MessageServerMessage{ + Sender: &MessageServerMessageSender{ + Type: "session", + SessionId: sender, + }, + Data: offer_data, + }, + } + + s.sendMessageUnlocked(response_message) +} + +func (s *ClientSession) sendCandidate(client McuClient, sender string, streamType StreamType, candidate interface{}) { + candidate_message := &AnswerOfferMessage{ + To: s.PublicId(), + From: sender, + Type: "candidate", + RoomType: string(streamType), + Payload: map[string]interface{}{ + "candidate": candidate, + }, + Sid: client.Sid(), + } + candidate_data, err := json.Marshal(candidate_message) + if err != nil { + log.Println("Could not serialize candidate", candidate_message, err) + return + } + response_message := &ServerMessage{ + Type: "message", + Message: &MessageServerMessage{ + Sender: &MessageServerMessageSender{ + Type: "session", + SessionId: sender, + }, + Data: candidate_data, + }, + } + + s.sendMessageUnlocked(response_message) +} + +func (s *ClientSession) sendMessageUnlocked(message *ServerMessage) bool { + if c := s.getClientUnlocked(); c != nil { + if c.SendMessage(message) { + return true + } + } + + s.storePendingMessage(message) + return true +} + +func (s *ClientSession) SendError(e *Error) bool { + message := &ServerMessage{ + Type: "error", + Error: e, + } + return s.SendMessage(message) +} + +func (s *ClientSession) SendMessage(message *ServerMessage) bool { + message = s.filterMessage(message) + if message == nil { + return true + } + + s.mu.Lock() + defer s.mu.Unlock() + + return s.sendMessageUnlocked(message) +} + +func (s *ClientSession) SendMessages(messages []*ServerMessage) bool { + s.mu.Lock() + defer s.mu.Unlock() + + for _, message := range messages { + s.sendMessageUnlocked(message) + } + return true +} + +func (s *ClientSession) OnUpdateOffer(client McuClient, offer map[string]interface{}) { + s.mu.Lock() + defer s.mu.Unlock() + + for _, sub := range s.subscribers { + if sub.Id() == client.Id() { + s.sendOffer(client, sub.Publisher(), client.StreamType(), offer) + return + } + } +} + +func (s *ClientSession) OnIceCandidate(client McuClient, candidate interface{}) { + s.mu.Lock() + defer s.mu.Unlock() + + for _, sub := range s.subscribers { + if sub.Id() == client.Id() { + s.sendCandidate(client, sub.Publisher(), client.StreamType(), candidate) + return + } + } + + for _, pub := range s.publishers { + if pub.Id() == client.Id() { + s.sendCandidate(client, s.PublicId(), client.StreamType(), candidate) + return + } + } + + log.Printf("Session %s received candidate %+v for unknown client %s", s.PublicId(), candidate, client.Id()) +} + +func (s *ClientSession) OnIceCompleted(client McuClient) { + // TODO(jojo): This causes a JavaScript error when creating a candidate from "null". + // Figure out a better way to signal this. + + // An empty candidate signals the end of candidates. + // s.OnIceCandidate(client, nil) +} + +func (s *ClientSession) SubscriberSidUpdated(subscriber McuSubscriber) { +} + +func (s *ClientSession) PublisherClosed(publisher McuPublisher) { + s.mu.Lock() + defer s.mu.Unlock() + + for id, p := range s.publishers { + if p == publisher { + delete(s.publishers, id) + break + } + } +} + +func (s *ClientSession) SubscriberClosed(subscriber McuSubscriber) { + s.mu.Lock() + defer s.mu.Unlock() + + for id, sub := range s.subscribers { + if sub == subscriber { + delete(s.subscribers, id) + break + } + } +} + +type PermissionError struct { + permission Permission +} + +func (e *PermissionError) Permission() Permission { + return e.permission +} + +func (e *PermissionError) Error() string { + return fmt.Sprintf("permission \"%s\" not found", e.permission) +} + +func (s *ClientSession) isSdpAllowedToSendLocked(sdp *sdp.SessionDescription) (MediaType, error) { + if sdp == nil { + // Should have already been checked when data was validated. + return 0, ErrNoSdp + } + + var mediaTypes MediaType + mayPublishMedia := s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_MEDIA) + for _, md := range sdp.MediaDescriptions { + switch md.MediaName.Media { + case "audio": + if !mayPublishMedia && !s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_AUDIO) { + return 0, &PermissionError{PERMISSION_MAY_PUBLISH_AUDIO} + } + + mediaTypes |= MediaTypeAudio + case "video": + if !mayPublishMedia && !s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_VIDEO) { + return 0, &PermissionError{PERMISSION_MAY_PUBLISH_VIDEO} + } + + mediaTypes |= MediaTypeVideo + } + } + + return mediaTypes, nil +} + +func (s *ClientSession) IsAllowedToSend(data *MessageClientMessageData) error { + s.mu.Lock() + defer s.mu.Unlock() + + if data != nil && data.RoomType == "screen" { + if s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_SCREEN) { + return nil + } + return &PermissionError{PERMISSION_MAY_PUBLISH_SCREEN} + } else if s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_MEDIA) { + // Client is allowed to publish any media (audio / video). + return nil + } else if data != nil && data.Type == "offer" { + // Check what user is trying to publish and check permissions accordingly. + if _, err := s.isSdpAllowedToSendLocked(data.offerSdp); err != nil { + return err + } + + return nil + } else { + // Candidate or unknown event, check if client is allowed to publish any media. + if s.hasAnyPermissionLocked(PERMISSION_MAY_PUBLISH_AUDIO, PERMISSION_MAY_PUBLISH_VIDEO) { + return nil + } + + return fmt.Errorf("permission check failed") + } +} + +func (s *ClientSession) CheckOfferType(streamType StreamType, data *MessageClientMessageData) (MediaType, error) { + s.mu.Lock() + defer s.mu.Unlock() + + return s.checkOfferTypeLocked(streamType, data) +} + +func (s *ClientSession) checkOfferTypeLocked(streamType StreamType, data *MessageClientMessageData) (MediaType, error) { + if streamType == StreamTypeScreen { + if !s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_SCREEN) { + return 0, &PermissionError{PERMISSION_MAY_PUBLISH_SCREEN} + } + + return MediaTypeScreen, nil + } else if data != nil && data.Type == "offer" { + mediaTypes, err := s.isSdpAllowedToSendLocked(data.offerSdp) + if err != nil { + return 0, err + } + + return mediaTypes, nil + } + + return 0, nil +} + +func (s *ClientSession) GetOrCreatePublisher(ctx context.Context, mcu Mcu, streamType StreamType, data *MessageClientMessageData) (McuPublisher, error) { + s.mu.Lock() + defer s.mu.Unlock() + + mediaTypes, err := s.checkOfferTypeLocked(streamType, data) + if err != nil { + return nil, err + } + + publisher, found := s.publishers[streamType] + if !found { + client := s.getClientUnlocked() + s.mu.Unlock() + defer s.mu.Lock() + + settings := NewPublisherSettings{ + Bitrate: data.Bitrate, + MediaTypes: mediaTypes, + + AudioCodec: data.AudioCodec, + VideoCodec: data.VideoCodec, + VP9Profile: data.VP9Profile, + H264Profile: data.H264Profile, + } + if backend := s.Backend(); backend != nil { + var maxBitrate int + if streamType == StreamTypeScreen { + maxBitrate = backend.maxScreenBitrate + } else { + maxBitrate = backend.maxStreamBitrate + } + if settings.Bitrate <= 0 { + settings.Bitrate = maxBitrate + } else if maxBitrate > 0 && settings.Bitrate > maxBitrate { + settings.Bitrate = maxBitrate + } + } + var err error + publisher, err = mcu.NewPublisher(ctx, s, s.PublicId(), data.Sid, streamType, settings, client) + if err != nil { + return nil, err + } + if s.publishers == nil { + s.publishers = make(map[StreamType]McuPublisher) + } + if prev, found := s.publishers[streamType]; found { + // Another thread created the publisher while we were waiting. + go func(pub McuPublisher) { + closeCtx := context.Background() + pub.Close(closeCtx) + }(publisher) + publisher = prev + } else { + s.publishers[streamType] = publisher + } + log.Printf("Publishing %s as %s for session %s", streamType, publisher.Id(), s.PublicId()) + s.publisherWaiters.Wakeup() + } else { + publisher.SetMedia(mediaTypes) + } + + return publisher, nil +} + +func (s *ClientSession) getPublisherLocked(streamType StreamType) McuPublisher { + return s.publishers[streamType] +} + +func (s *ClientSession) GetPublisher(streamType StreamType) McuPublisher { + s.mu.Lock() + defer s.mu.Unlock() + + return s.getPublisherLocked(streamType) +} + +func (s *ClientSession) GetOrWaitForPublisher(ctx context.Context, streamType StreamType) McuPublisher { + s.mu.Lock() + defer s.mu.Unlock() + + publisher := s.getPublisherLocked(streamType) + if publisher != nil { + return publisher + } + + ch := make(chan struct{}, 1) + id := s.publisherWaiters.Add(ch) + defer s.publisherWaiters.Remove(id) + + for { + s.mu.Unlock() + select { + case <-ch: + s.mu.Lock() + publisher := s.getPublisherLocked(streamType) + if publisher != nil { + return publisher + } + case <-ctx.Done(): + s.mu.Lock() + return nil + } + } +} + +func (s *ClientSession) GetOrCreateSubscriber(ctx context.Context, mcu Mcu, id string, streamType StreamType) (McuSubscriber, error) { + s.mu.Lock() + defer s.mu.Unlock() + + // TODO(jojo): Add method to remove subscribers. + + subscriber, found := s.subscribers[getStreamId(id, streamType)] + if !found { + client := s.getClientUnlocked() + s.mu.Unlock() + var err error + subscriber, err = mcu.NewSubscriber(ctx, s, id, streamType, client) + s.mu.Lock() + if err != nil { + return nil, err + } + if s.subscribers == nil { + s.subscribers = make(map[string]McuSubscriber) + } + if prev, found := s.subscribers[getStreamId(id, streamType)]; found { + // Another thread created the subscriber while we were waiting. + go func(sub McuSubscriber) { + closeCtx := context.Background() + sub.Close(closeCtx) + }(subscriber) + subscriber = prev + } else { + s.subscribers[getStreamId(id, streamType)] = subscriber + } + log.Printf("Subscribing %s from %s as %s in session %s", streamType, id, subscriber.Id(), s.PublicId()) + } + + return subscriber, nil +} + +func (s *ClientSession) GetSubscriber(id string, streamType StreamType) McuSubscriber { + s.mu.Lock() + defer s.mu.Unlock() + + return s.subscribers[getStreamId(id, streamType)] +} + +func (s *ClientSession) ProcessAsyncRoomMessage(message *AsyncMessage) { + s.processAsyncMessage(message) +} + +func (s *ClientSession) ProcessAsyncUserMessage(message *AsyncMessage) { + s.processAsyncMessage(message) +} + +func (s *ClientSession) ProcessAsyncSessionMessage(message *AsyncMessage) { + s.processAsyncMessage(message) +} + +func (s *ClientSession) processAsyncMessage(message *AsyncMessage) { + switch message.Type { + case "permissions": + s.SetPermissions(message.Permissions) + go func() { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_MEDIA) { + if publisher, found := s.publishers[StreamTypeVideo]; found { + if (publisher.HasMedia(MediaTypeAudio) && !s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_AUDIO)) || + (publisher.HasMedia(MediaTypeVideo) && !s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_VIDEO)) { + delete(s.publishers, StreamTypeVideo) + log.Printf("Session %s is no longer allowed to publish media, closing publisher %s", s.PublicId(), publisher.Id()) + go func() { + publisher.Close(context.Background()) + }() + return + } + } + } + if !s.hasPermissionLocked(PERMISSION_MAY_PUBLISH_SCREEN) { + if publisher, found := s.publishers[StreamTypeScreen]; found { + delete(s.publishers, StreamTypeScreen) + log.Printf("Session %s is no longer allowed to publish screen, closing publisher %s", s.PublicId(), publisher.Id()) + go func() { + publisher.Close(context.Background()) + }() + return + } + } + }() + return + case "message": + if message.Message.Type == "bye" && message.Message.Bye.Reason == "room_session_reconnected" { + log.Printf("Closing session %s because same room session %s connected", s.PublicId(), s.RoomSessionId()) + s.LeaveRoom(false) + defer s.closeAndWait(false) + } + case "sendoffer": + // Process asynchronously to not block other messages received. + go func() { + ctx, cancel := context.WithTimeout(s.Context(), s.hub.mcuTimeout) + defer cancel() + + mc, err := s.GetOrCreateSubscriber(ctx, s.hub.mcu, message.SendOffer.SessionId, StreamType(message.SendOffer.Data.RoomType)) + if err != nil { + log.Printf("Could not create MCU subscriber for session %s to process sendoffer in %s: %s", message.SendOffer.SessionId, s.PublicId(), err) + if err := s.events.PublishSessionMessage(message.SendOffer.SessionId, s.backend, &AsyncMessage{ + Type: "message", + Message: &ServerMessage{ + Id: message.SendOffer.MessageId, + Type: "error", + Error: NewError("client_not_found", "No MCU client found to send message to."), + }, + }); err != nil { + log.Printf("Error sending sendoffer error response to %s: %s", message.SendOffer.SessionId, err) + } + return + } else if mc == nil { + log.Printf("No MCU subscriber found for session %s to process sendoffer in %s", message.SendOffer.SessionId, s.PublicId()) + if err := s.events.PublishSessionMessage(message.SendOffer.SessionId, s.backend, &AsyncMessage{ + Type: "message", + Message: &ServerMessage{ + Id: message.SendOffer.MessageId, + Type: "error", + Error: NewError("client_not_found", "No MCU client found to send message to."), + }, + }); err != nil { + log.Printf("Error sending sendoffer error response to %s: %s", message.SendOffer.SessionId, err) + } + return + } + + mc.SendMessage(s.Context(), nil, message.SendOffer.Data, func(err error, response map[string]interface{}) { + if err != nil { + log.Printf("Could not send MCU message %+v for session %s to %s: %s", message.SendOffer.Data, message.SendOffer.SessionId, s.PublicId(), err) + if err := s.events.PublishSessionMessage(message.SendOffer.SessionId, s.backend, &AsyncMessage{ + Type: "message", + Message: &ServerMessage{ + Id: message.SendOffer.MessageId, + Type: "error", + Error: NewError("processing_failed", "Processing of the message failed, please check server logs."), + }, + }); err != nil { + log.Printf("Error sending sendoffer error response to %s: %s", message.SendOffer.SessionId, err) + } + return + } else if response == nil { + // No response received + return + } + + s.hub.sendMcuMessageResponse(s, mc, &MessageClientMessage{ + Recipient: MessageClientMessageRecipient{ + SessionId: message.SendOffer.SessionId, + }, + }, message.SendOffer.Data, response) + }) + }() + return + } + + serverMessage := s.filterAsyncMessage(message) + if serverMessage == nil { + return + } + + s.SendMessage(serverMessage) +} + +func (s *ClientSession) storePendingMessage(message *ServerMessage) { + if message.IsChatRefresh() { + if s.hasPendingChat { + // Only send a single "chat-refresh" message on resume. + return + } + + s.hasPendingChat = true + } + if !s.hasPendingParticipantsUpdate && message.IsParticipantsUpdate() { + s.hasPendingParticipantsUpdate = true + } + s.pendingClientMessages = append(s.pendingClientMessages, message) + if len(s.pendingClientMessages) >= warnPendingMessagesCount { + log.Printf("Session %s has %d pending messages", s.PublicId(), len(s.pendingClientMessages)) + } +} + +func filterDisplayNames(events []*EventServerMessageSessionEntry) []*EventServerMessageSessionEntry { + result := make([]*EventServerMessageSessionEntry, 0, len(events)) + for _, event := range events { + if len(event.User) == 0 { + result = append(result, event) + continue + } + + var userdata map[string]interface{} + if err := json.Unmarshal(event.User, &userdata); err != nil { + result = append(result, event) + continue + } + + if _, found := userdata["displayname"]; !found { + result = append(result, event) + continue + } + + delete(userdata, "displayname") + if len(userdata) == 0 { + // No more userdata, no need to serialize empty map. + e := event.Clone() + e.User = nil + result = append(result, e) + continue + } + + data, err := json.Marshal(userdata) + if err != nil { + result = append(result, event) + continue + } + + e := event.Clone() + e.User = data + result = append(result, e) + } + return result +} + +func (s *ClientSession) filterDuplicateJoin(entries []*EventServerMessageSessionEntry) []*EventServerMessageSessionEntry { + s.seenJoinedLock.Lock() + defer s.seenJoinedLock.Unlock() + + // Due to the asynchronous events, a session might received a "Joined" event + // for the same (other) session twice, so filter these out on a per-session + // level. + result := make([]*EventServerMessageSessionEntry, 0, len(entries)) + for _, e := range entries { + if s.seenJoinedEvents[e.SessionId] { + log.Printf("Session %s got duplicate joined event for %s, ignoring", s.publicId, e.SessionId) + continue + } + + if s.seenJoinedEvents == nil { + s.seenJoinedEvents = make(map[string]bool) + } + s.seenJoinedEvents[e.SessionId] = true + result = append(result, e) + } + return result +} + +func (s *ClientSession) filterMessage(message *ServerMessage) *ServerMessage { + switch message.Type { + case "event": + switch message.Event.Target { + case "participants": + if message.Event.Type == "update" { + m := message.Event.Update + users := make(map[string]bool) + for _, entry := range m.Users { + users[entry["sessionId"].(string)] = true + } + for _, entry := range m.Changed { + if users[entry["sessionId"].(string)] { + continue + } + m.Users = append(m.Users, entry) + } + // TODO(jojo): Only send all users if current session id has + // changed its "inCall" flag to true. + m.Changed = nil + } + case "room": + switch message.Event.Type { + case "join": + join := s.filterDuplicateJoin(message.Event.Join) + if len(join) == 0 { + return nil + } + copied := false + if len(join) != len(message.Event.Join) { + // Create unique copy of message for only this client. + copied = true + message = &ServerMessage{ + Id: message.Id, + Type: message.Type, + Event: &EventServerMessage{ + Type: message.Event.Type, + Target: message.Event.Target, + Join: join, + }, + } + } + + if s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) { + if copied { + message.Event.Join = filterDisplayNames(message.Event.Join) + } else { + message = &ServerMessage{ + Id: message.Id, + Type: message.Type, + Event: &EventServerMessage{ + Type: message.Event.Type, + Target: message.Event.Target, + Join: filterDisplayNames(message.Event.Join), + }, + } + } + } + case "leave": + s.seenJoinedLock.Lock() + defer s.seenJoinedLock.Unlock() + + for _, e := range message.Event.Leave { + delete(s.seenJoinedEvents, e) + } + case "message": + if message.Event.Message == nil || len(message.Event.Message.Data) == 0 || !s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) { + return message + } + + var data RoomEventMessageData + if err := json.Unmarshal(message.Event.Message.Data, &data); err != nil { + return message + } + + if data.Type == "chat" && data.Chat != nil && data.Chat.Comment != nil { + if displayName, found := (*data.Chat.Comment)["actorDisplayName"]; found && displayName != "" { + (*data.Chat.Comment)["actorDisplayName"] = "" + if encoded, err := json.Marshal(data); err == nil { + // Create unique copy of message for only this client. + message = &ServerMessage{ + Id: message.Id, + Type: message.Type, + Event: &EventServerMessage{ + Type: message.Event.Type, + Target: message.Event.Target, + Message: &RoomEventMessage{ + RoomId: message.Event.Message.RoomId, + Data: encoded, + }, + }, + } + } + } + } + } + } + case "message": + if message.Message != nil && len(message.Message.Data) > 0 && s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) { + var data MessageServerMessageData + if err := json.Unmarshal(message.Message.Data, &data); err != nil { + return message + } + + if data.Type == "nickChanged" { + return nil + } + } + } + + return message +} + +func (s *ClientSession) filterAsyncMessage(msg *AsyncMessage) *ServerMessage { + switch msg.Type { + case "message": + if msg.Message == nil { + log.Printf("Received asynchronous message without payload: %+v", msg) + return nil + } + + switch msg.Message.Type { + case "message": + if msg.Message.Message != nil { + if sender := msg.Message.Message.Sender; sender != nil { + if sender.SessionId == s.PublicId() { + // Don't send message back to sender (can happen if sent to user or room) + return nil + } + if sender.Type == RecipientTypeCall { + if room := s.GetRoom(); room == nil || !room.IsSessionInCall(s) { + // Session is not in call, so discard. + return nil + } + } + } + } + case "control": + if msg.Message.Control != nil { + if sender := msg.Message.Control.Sender; sender != nil { + if sender.SessionId == s.PublicId() { + // Don't send message back to sender (can happen if sent to user or room) + return nil + } + if sender.Type == RecipientTypeCall { + if room := s.GetRoom(); room == nil || !room.IsSessionInCall(s) { + // Session is not in call, so discard. + return nil + } + } + } + } + case "event": + if msg.Message.Event.Target == "room" { + // Can happen mostly during tests where an older room async message + // could be received by a subscriber that joined after it was sent. + if joined := s.getRoomJoinTime(); joined.IsZero() || msg.SendTime.Before(joined) { + log.Printf("Message %+v was sent on %s before room was joined on %s, ignoring", msg.Message, msg.SendTime, joined) + return nil + } + } + } + + return msg.Message + default: + log.Printf("Received async message with unsupported type %s: %+v", msg.Type, msg) + return nil + } +} + +func (s *ClientSession) NotifySessionResumed(client HandlerClient) { + s.mu.Lock() + if len(s.pendingClientMessages) == 0 { + s.mu.Unlock() + if room := s.GetRoom(); room != nil { + room.NotifySessionResumed(s) + } + return + } + + messages := s.pendingClientMessages + hasPendingParticipantsUpdate := s.hasPendingParticipantsUpdate + s.pendingClientMessages = nil + s.hasPendingChat = false + s.hasPendingParticipantsUpdate = false + s.mu.Unlock() + + log.Printf("Send %d pending messages to session %s", len(messages), s.PublicId()) + // Send through session to handle connection interruptions. + s.SendMessages(messages) + + if !hasPendingParticipantsUpdate { + // Only need to send initial participants list update if none was part of the pending messages. + if room := s.GetRoom(); room != nil { + room.NotifySessionResumed(s) + } + } +} + +func (s *ClientSession) AddVirtualSession(session *VirtualSession) { + s.mu.Lock() + if s.virtualSessions == nil { + s.virtualSessions = make(map[*VirtualSession]bool) + } + s.virtualSessions[session] = true + s.mu.Unlock() +} + +func (s *ClientSession) RemoveVirtualSession(session *VirtualSession) { + s.mu.Lock() + delete(s.virtualSessions, session) + s.mu.Unlock() +} + +func (s *ClientSession) GetVirtualSessions() []*VirtualSession { + s.mu.Lock() + defer s.mu.Unlock() + + result := make([]*VirtualSession, 0, len(s.virtualSessions)) + for session := range s.virtualSessions { + result = append(result, session) + } + return result +} + +func (s *ClientSession) HandleResponse(id string, handler ResponseHandlerFunc) { + s.responseHandlersLock.Lock() + defer s.responseHandlersLock.Unlock() + + if s.responseHandlers == nil { + s.responseHandlers = make(map[string]ResponseHandlerFunc) + } + + s.responseHandlers[id] = handler +} + +func (s *ClientSession) ClearResponseHandler(id string) { + s.responseHandlersLock.Lock() + defer s.responseHandlersLock.Unlock() + + delete(s.responseHandlers, id) +} + +func (s *ClientSession) ProcessResponse(message *ClientMessage) bool { + id := message.Id + if id == "" { + return false + } + + s.responseHandlersLock.Lock() + cb, found := s.responseHandlers[id] + defer s.responseHandlersLock.Unlock() + + if !found { + return false + } + + return cb(message) +} diff --git a/clientsession_test.go b/clientsession_test.go new file mode 100644 index 0000000..e75b132 --- /dev/null +++ b/clientsession_test.go @@ -0,0 +1,267 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2019 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + equalStrings = map[bool]string{ + true: "equal", + false: "not equal", + } +) + +type EqualTestData struct { + a map[Permission]bool + b map[Permission]bool + equal bool +} + +func Test_permissionsEqual(t *testing.T) { + tests := []EqualTestData{ + { + a: nil, + b: nil, + equal: true, + }, + { + a: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + }, + b: nil, + equal: false, + }, + { + a: nil, + b: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + }, + equal: false, + }, + { + a: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + }, + b: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + }, + equal: true, + }, + { + a: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + PERMISSION_MAY_PUBLISH_SCREEN: true, + }, + b: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + }, + equal: false, + }, + { + a: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + }, + b: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + PERMISSION_MAY_PUBLISH_SCREEN: true, + }, + equal: false, + }, + { + a: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + PERMISSION_MAY_PUBLISH_SCREEN: true, + }, + b: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + PERMISSION_MAY_PUBLISH_SCREEN: true, + }, + equal: true, + }, + { + a: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + PERMISSION_MAY_PUBLISH_SCREEN: true, + }, + b: map[Permission]bool{ + PERMISSION_MAY_PUBLISH_MEDIA: true, + PERMISSION_MAY_PUBLISH_SCREEN: false, + }, + equal: false, + }, + } + for idx, test := range tests { + test := test + t.Run(strconv.Itoa(idx), func(t *testing.T) { + t.Parallel() + equal := permissionsEqual(test.a, test.b) + assert.Equal(t, test.equal, equal, "Expected %+v to be %s to %+v but was %s", test.a, equalStrings[test.equal], test.b, equalStrings[equal]) + }) + } +} + +func TestBandwidth_Client(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mcu, err := NewTestMCU() + require.NoError(err) + require.NoError(mcu.Start(ctx)) + defer mcu.Stop() + + hub.SetMcu(mcu) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event. + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) + + // Client may not send an offer with audio and video. + bitrate := 10000 + require.NoError(client.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello.Hello.SessionId, + }, MessageClientMessageData{ + Type: "offer", + Sid: "54321", + RoomType: "video", + Bitrate: bitrate, + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, + }, + })) + + require.NoError(client.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo)) + + pub := mcu.GetPublisher(hello.Hello.SessionId) + require.NotNil(pub) + assert.Equal(bitrate, pub.settings.Bitrate) +} + +func TestBandwidth_Backend(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + hub, _, _, server := CreateHubWithMultipleBackendsForTest(t) + + u, err := url.Parse(server.URL + "/one") + require.NoError(t, err) + backend := hub.backend.GetBackend(u) + require.NotNil(t, backend, "Could not get backend") + + backend.maxScreenBitrate = 1000 + backend.maxStreamBitrate = 2000 + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mcu, err := NewTestMCU() + require.NoError(t, err) + require.NoError(t, mcu.Start(ctx)) + defer mcu.Stop() + + hub.SetMcu(mcu) + + streamTypes := []StreamType{ + StreamTypeVideo, + StreamTypeScreen, + } + + for _, streamType := range streamTypes { + t.Run(string(streamType), func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + params := TestBackendClientAuthParams{ + UserId: testDefaultUserId, + } + require.NoError(client.SendHelloParams(server.URL+"/one", HelloVersionV1, "client", nil, params)) + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event. + require.NoError(client.RunUntilJoined(ctx, hello.Hello)) + + // Client may not send an offer with audio and video. + bitrate := 10000 + require.NoError(client.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello.Hello.SessionId, + }, MessageClientMessageData{ + Type: "offer", + Sid: "54321", + RoomType: string(streamType), + Bitrate: bitrate, + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, + }, + })) + + require.NoError(client.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo)) + + pub := mcu.GetPublisher(hello.Hello.SessionId) + require.NotNil(pub, "Could not find publisher") + + var expectBitrate int + if streamType == StreamTypeVideo { + expectBitrate = backend.maxStreamBitrate + } else { + expectBitrate = backend.maxScreenBitrate + } + assert.Equal(expectBitrate, pub.settings.Bitrate) + }) + } +} diff --git a/internal/closer.go b/closer.go similarity index 98% rename from internal/closer.go rename to closer.go index 62ed06f..ea00769 100644 --- a/internal/closer.go +++ b/closer.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( "sync/atomic" diff --git a/internal/closer_test.go b/closer_test.go similarity index 93% rename from internal/closer_test.go rename to closer_test.go index 519a7cc..ab23621 100644 --- a/internal/closer_test.go +++ b/closer_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( "sync" @@ -29,15 +29,16 @@ import ( ) func TestCloserMulti(t *testing.T) { - t.Parallel() closer := NewCloser() var wg sync.WaitGroup count := 10 - for range count { - wg.Go(func() { + for i := 0; i < count; i++ { + wg.Add(1) + go func() { + defer wg.Done() <-closer.C - }) + }() } assert.False(t, closer.IsClosed()) @@ -47,7 +48,6 @@ func TestCloserMulti(t *testing.T) { } func TestCloserCloseBeforeWait(t *testing.T) { - t.Parallel() closer := NewCloser() closer.Close() assert.True(t, closer.IsClosed()) diff --git a/cmd/client/stats.go b/cmd/client/stats.go deleted file mode 100644 index a7acc99..0000000 --- a/cmd/client/stats.go +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package main - -import ( - "log" - "sync/atomic" - "time" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" -) - -type Stats struct { - numRecvMessages atomic.Int64 - numSentMessages atomic.Int64 - resetRecvMessages int64 - resetSentMessages int64 - numRecvBytes atomic.Uint64 - numSentBytes atomic.Uint64 - resetRecvBytes uint64 - resetSentBytes uint64 - - start time.Time -} - -func (s *Stats) reset(start time.Time) { - s.resetRecvMessages = s.numRecvMessages.Load() - s.resetSentMessages = s.numSentMessages.Load() - s.resetRecvBytes = s.numRecvBytes.Load() - s.resetSentBytes = s.numSentBytes.Load() - s.start = start -} - -type statsLogEntries struct { - totalSentMessages int64 - sentMessagesPerSec int64 - sentBytesPerSec api.Bandwidth - - totalRecvMessages int64 - recvMessagesPerSec int64 - recvBytesPerSec api.Bandwidth -} - -func (s *Stats) getLogEntries(now time.Time) *statsLogEntries { - duration := now.Sub(s.start) - perSec := int64(duration / time.Second) - if perSec == 0 { - return nil - } - - totalSentMessages := s.numSentMessages.Load() - sentMessages := totalSentMessages - s.resetSentMessages - sentBytes := api.BandwidthFromBytes(s.numSentBytes.Load() - s.resetSentBytes) - totalRecvMessages := s.numRecvMessages.Load() - recvMessages := totalRecvMessages - s.resetRecvMessages - recvBytes := api.BandwidthFromBytes(s.numRecvBytes.Load() - s.resetRecvBytes) - - s.reset(now) - return &statsLogEntries{ - totalSentMessages: totalSentMessages, - sentMessagesPerSec: sentMessages / perSec, - sentBytesPerSec: sentBytes, - - totalRecvMessages: totalRecvMessages, - recvMessagesPerSec: recvMessages / perSec, - recvBytesPerSec: recvBytes, - } -} - -func (s *Stats) Log() { - now := time.Now() - if entries := s.getLogEntries(now); entries != nil { - log.Printf("Stats: sent=%d (%d/sec, %s), recv=%d (%d/sec, %s), delta=%d", - entries.totalSentMessages, entries.sentMessagesPerSec, entries.sentBytesPerSec, - entries.totalRecvMessages, entries.recvMessagesPerSec, entries.recvBytesPerSec, - entries.totalSentMessages-entries.totalRecvMessages) - } -} diff --git a/cmd/client/stats_test.go b/cmd/client/stats_test.go deleted file mode 100644 index bb3e492..0000000 --- a/cmd/client/stats_test.go +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package main - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" -) - -func TestStats(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - var stats Stats - assert.Nil(stats.getLogEntries(time.Time{})) - now := time.Now() - if entries := stats.getLogEntries(now); assert.NotNil(entries) { - assert.EqualValues(0, entries.totalSentMessages) - assert.EqualValues(0, entries.sentMessagesPerSec) - assert.EqualValues(0, entries.sentBytesPerSec) - - assert.EqualValues(0, entries.totalRecvMessages) - assert.EqualValues(0, entries.recvMessagesPerSec) - assert.EqualValues(0, entries.recvBytesPerSec) - } - - stats.numSentMessages.Add(10) - stats.numSentBytes.Add((api.Bandwidth(20) * api.Kilobit).Bits()) - - stats.numRecvMessages.Add(30) - stats.numRecvBytes.Add((api.Bandwidth(40) * api.Kilobit).Bits()) - - if entries := stats.getLogEntries(now.Add(time.Second)); assert.NotNil(entries) { - assert.EqualValues(10, entries.totalSentMessages) - assert.EqualValues(10, entries.sentMessagesPerSec) - assert.EqualValues(20*1024*8, entries.sentBytesPerSec) - - assert.EqualValues(30, entries.totalRecvMessages) - assert.EqualValues(30, entries.recvMessagesPerSec) - assert.EqualValues(40*1024*8, entries.recvBytesPerSec) - } - - stats.numSentMessages.Add(100) - stats.numSentBytes.Add((api.Bandwidth(200) * api.Kilobit).Bits()) - - stats.numRecvMessages.Add(300) - stats.numRecvBytes.Add((api.Bandwidth(400) * api.Kilobit).Bits()) - - if entries := stats.getLogEntries(now.Add(2 * time.Second)); assert.NotNil(entries) { - assert.EqualValues(110, entries.totalSentMessages) - assert.EqualValues(100, entries.sentMessagesPerSec) - assert.EqualValues(200*1024*8, entries.sentBytesPerSec) - - assert.EqualValues(330, entries.totalRecvMessages) - assert.EqualValues(300, entries.recvMessagesPerSec) - assert.EqualValues(400*1024*8, entries.recvBytesPerSec) - } -} diff --git a/cmd/proxy/proxy_remote_test.go b/cmd/proxy/proxy_remote_test.go deleted file mode 100644 index 0b3f41b..0000000 --- a/cmd/proxy/proxy_remote_test.go +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package main - -import ( - "context" - "net" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" -) - -func (c *RemoteConnection) WaitForConnection(ctx context.Context) error { - c.mu.Lock() - defer c.mu.Unlock() - - // Only used in tests, so a busy-loop should be fine. - for c.conn == nil || c.connectedSince.IsZero() || !c.helloReceived { - if err := ctx.Err(); err != nil { - return err - } - - c.mu.Unlock() - time.Sleep(time.Nanosecond) - c.mu.Lock() - } - - return nil -} - -func (c *RemoteConnection) WaitForDisconnect(ctx context.Context) error { - c.mu.Lock() - defer c.mu.Unlock() - - initial := c.conn - if initial == nil { - return nil - } - - // Only used in tests, so a busy-loop should be fine. - for c.conn == initial { - if err := ctx.Err(); err != nil { - return err - } - - c.mu.Unlock() - time.Sleep(time.Nanosecond) - c.mu.Lock() - } - return nil -} - -func Test_ProxyRemoteConnectionReconnect(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - - server, key, httpserver := newProxyServerForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - conn, err := NewRemoteConnection(server, httpserver.URL, TokenIdForTest, key, nil) - require.NoError(err) - t.Cleanup(func() { - assert.NoError(conn.SendBye()) - assert.NoError(conn.Close()) - }) - - assert.NoError(conn.WaitForConnection(ctx)) - - // Closing the connection will reconnect automatically - conn.mu.Lock() - c := conn.conn - conn.mu.Unlock() - assert.NoError(c.Close()) - assert.NoError(conn.WaitForDisconnect(ctx)) - assert.NoError(conn.WaitForConnection(ctx)) -} - -func Test_ProxyRemoteConnectionReconnectUnknownSession(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - - server, key, httpserver := newProxyServerForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - conn, err := NewRemoteConnection(server, httpserver.URL, TokenIdForTest, key, nil) - require.NoError(err) - t.Cleanup(func() { - assert.NoError(conn.SendBye()) - assert.NoError(conn.Close()) - }) - - assert.NoError(conn.WaitForConnection(ctx)) - - // Closing the connection will reconnect automatically - conn.mu.Lock() - c := conn.conn - sessionId := conn.sessionId - conn.mu.Unlock() - var sid uint64 - server.IterateSessions(func(session *ProxySession) { - if session.PublicId() == sessionId { - sid = session.Sid() - } - }) - require.NotEqualValues(0, sid) - server.DeleteSession(sid) - if err := c.Close(); err != nil { - // If an error occurs while closing, it may only be "use of closed network - // connection" because the "DeleteSession" might have already closed the - // socket. - assert.ErrorIs(err, net.ErrClosed) - } - assert.NoError(conn.WaitForDisconnect(ctx)) - assert.NoError(conn.WaitForConnection(ctx)) - assert.NotEqual(sessionId, conn.SessionId()) -} - -func Test_ProxyRemoteConnectionReconnectExpiredSession(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - - server, key, httpserver := newProxyServerForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - conn, err := NewRemoteConnection(server, httpserver.URL, TokenIdForTest, key, nil) - require.NoError(err) - t.Cleanup(func() { - assert.NoError(conn.SendBye()) - assert.NoError(conn.Close()) - }) - - assert.NoError(conn.WaitForConnection(ctx)) - - // Closing the connection will reconnect automatically - conn.mu.Lock() - sessionId := conn.sessionId - conn.mu.Unlock() - var session *ProxySession - server.IterateSessions(func(sess *ProxySession) { - if sess.PublicId() == sessionId { - session = sess - } - }) - require.NotNil(session) - session.Close() - assert.NoError(conn.WaitForDisconnect(ctx)) - assert.NoError(conn.WaitForConnection(ctx)) - assert.NotEqual(sessionId, conn.SessionId()) -} - -func Test_ProxyRemoteConnectionCreatePublisher(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - - server, key, httpserver := newProxyServerForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - conn, err := NewRemoteConnection(server, httpserver.URL, TokenIdForTest, key, nil) - require.NoError(err) - t.Cleanup(func() { - assert.NoError(conn.SendBye()) - assert.NoError(conn.Close()) - }) - - publisherId := "the-publisher" - hostname := "the-hostname" - port := 1234 - rtcpPort := 2345 - - _, err = conn.RequestMessage(ctx, &proxy.ClientMessage{ - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "publish-remote", - ClientId: publisherId, - Hostname: hostname, - Port: port, - RtcpPort: rtcpPort, - }, - }) - assert.ErrorContains(err, UnknownClient.Error()) -} diff --git a/cmd/proxy/proxy_tokens_static_test.go b/cmd/proxy/proxy_tokens_static_test.go deleted file mode 100644 index a7bc0fa..0000000 --- a/cmd/proxy/proxy_tokens_static_test.go +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package main - -import ( - "crypto/rand" - "crypto/rsa" - "os" - "path" - "testing" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" -) - -func TestStaticTokens(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - filename := path.Join(t.TempDir(), "token.pub") - - key1, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - require.NoError(internal.WritePublicKey(&key1.PublicKey, filename)) - - logger := logtest.NewLoggerForTest(t) - config := goconf.NewConfigFile() - config.AddOption("tokens", "foo", filename) - - tokens, err := NewProxyTokensStatic(logger, config) - require.NoError(err) - - defer tokens.Close() - - if token, err := tokens.Get("foo"); assert.NoError(err) { - assert.Equal("foo", token.id) - assert.True(key1.PublicKey.Equal(token.key)) - } - - key2, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - require.NoError(internal.WritePublicKey(&key2.PublicKey, filename)) - - tokens.Reload(config) - - if token, err := tokens.Get("foo"); assert.NoError(err) { - assert.Equal("foo", token.id) - assert.True(key2.PublicKey.Equal(token.key)) - } -} - -func testStaticTokensMissing(t *testing.T, reload bool) { - require := require.New(t) - assert := assert.New(t) - - filename := path.Join(t.TempDir(), "token.pub") - - logger := logtest.NewLoggerForTest(t) - config := goconf.NewConfigFile() - if !reload { - config.AddOption("tokens", "foo", filename) - } - - tokens, err := NewProxyTokensStatic(logger, config) - if !reload { - assert.ErrorIs(err, os.ErrNotExist) - return - } - - require.NoError(err) - defer tokens.Close() - - config.AddOption("tokens", "foo", filename) - tokens.Reload(config) -} - -func TestStaticTokensMissing(t *testing.T) { - t.Parallel() - - testStaticTokensMissing(t, false) -} - -func TestStaticTokensMissingReload(t *testing.T) { - t.Parallel() - - testStaticTokensMissing(t, true) -} - -func testStaticTokensEmpty(t *testing.T, reload bool) { - require := require.New(t) - assert := assert.New(t) - - logger := logtest.NewLoggerForTest(t) - config := goconf.NewConfigFile() - if !reload { - config.AddOption("tokens", "foo", "") - } - - tokens, err := NewProxyTokensStatic(logger, config) - if !reload { - assert.ErrorContains(err, "no filename given") - return - } - - require.NoError(err) - defer tokens.Close() - - config.AddOption("tokens", "foo", "") - tokens.Reload(config) -} - -func TestStaticTokensEmpty(t *testing.T) { - t.Parallel() - - testStaticTokensEmpty(t, false) -} - -func TestStaticTokensEmptyReload(t *testing.T) { - t.Parallel() - - testStaticTokensEmpty(t, true) -} - -func testStaticTokensInvalidData(t *testing.T, reload bool) { - require := require.New(t) - assert := assert.New(t) - - filename := path.Join(t.TempDir(), "token.pub") - require.NoError(os.WriteFile(filename, []byte("invalid-key-data"), 0600)) - - logger := logtest.NewLoggerForTest(t) - config := goconf.NewConfigFile() - if !reload { - config.AddOption("tokens", "foo", filename) - } - - tokens, err := NewProxyTokensStatic(logger, config) - if !reload { - assert.ErrorContains(err, "could not parse public key") - return - } - - require.NoError(err) - defer tokens.Close() - - config.AddOption("tokens", "foo", filename) - tokens.Reload(config) -} - -func TestStaticTokensInvalidData(t *testing.T) { - t.Parallel() - - testStaticTokensInvalidData(t, false) -} - -func TestStaticTokensInvalidDataReload(t *testing.T) { - t.Parallel() - - testStaticTokensInvalidData(t, true) -} diff --git a/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index cd93789..0000000 --- a/cmd/server/main.go +++ /dev/null @@ -1,437 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package main - -import ( - "context" - "crypto/tls" - "errors" - "flag" - "fmt" - "log" - "net" - "net/http" - "os" - "os/signal" - "runtime" - runtimepprof "runtime/pprof" - "sync" - "syscall" - "time" - - "github.com/dlintw/goconf" - "github.com/gorilla/mux" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - signalinglog "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - "github.com/strukturag/nextcloud-spreed-signaling/v2/server" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/proxy" -) - -var ( - version = "unreleased" - - configFlag = flag.String("config", "server.conf", "config file to use") - - cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") - - memprofile = flag.String("memprofile", "", "write memory profile to file") - - showVersion = flag.Bool("version", false, "show version and quit") -) - -const ( - defaultReadTimeout = 15 - defaultWriteTimeout = 30 - - initialMcuRetry = time.Second - maxMcuRetry = time.Second * 16 - - dnsMonitorInterval = time.Second -) - -func createListener(addr string) (net.Listener, error) { - if addr[0] == '/' { - os.Remove(addr) - return net.Listen("unix", addr) - } - - return net.Listen("tcp", addr) -} - -func createTLSListener(addr string, certFile, keyFile string) (net.Listener, error) { - cert, err := tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - return nil, err - } - config := tls.Config{ - Certificates: []tls.Certificate{cert}, - } - if addr[0] == '/' { - os.Remove(addr) - return tls.Listen("unix", addr, &config) - } - - return tls.Listen("tcp", addr, &config) -} - -type Listeners struct { - logger signalinglog.Logger // +checklocksignore - mu sync.Mutex - // +checklocks:mu - listeners []net.Listener -} - -func (l *Listeners) Add(listener net.Listener) { - l.mu.Lock() - defer l.mu.Unlock() - - l.listeners = append(l.listeners, listener) -} - -func (l *Listeners) Close() { - l.mu.Lock() - defer l.mu.Unlock() - - for _, listener := range l.listeners { - if err := listener.Close(); err != nil { - l.logger.Printf("Error closing listener %s: %s", listener.Addr(), err) - } - } -} - -func main() { - log.SetFlags(log.Lshortfile) - flag.Parse() - - if *showVersion { - fmt.Printf("nextcloud-spreed-signaling version %s/%s\n", version, runtime.Version()) - os.Exit(0) - } - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGHUP) - signal.Notify(sigChan, syscall.SIGUSR1) - - stopCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - - logger := log.Default() - stopCtx = signalinglog.NewLoggerContext(stopCtx, logger) - - if *cpuprofile != "" { - f, err := os.Create(*cpuprofile) - if err != nil { - logger.Fatal(err) - } - - if err := runtimepprof.StartCPUProfile(f); err != nil { - logger.Fatalf("Error writing CPU profile to %s: %s", *cpuprofile, err) - } - logger.Printf("Writing CPU profile to %s ...", *cpuprofile) - defer runtimepprof.StopCPUProfile() - } - - if *memprofile != "" { - f, err := os.Create(*memprofile) - if err != nil { - logger.Fatal(err) - } - - defer func() { - logger.Printf("Writing Memory profile to %s ...", *memprofile) - runtime.GC() - if err := runtimepprof.WriteHeapProfile(f); err != nil { - logger.Printf("Error writing Memory profile to %s: %s", *memprofile, err) - } - }() - } - - logger.Printf("Starting up version %s/%s as pid %d", version, runtime.Version(), os.Getpid()) - - cfg, err := goconf.ReadConfigFile(*configFlag) - if err != nil { - logger.Fatal("Could not read configuration: ", err) - } - - logger.Printf("Using a maximum of %d CPUs", runtime.GOMAXPROCS(0)) - - server.RegisterStats() - - natsUrl, _ := config.GetStringOptionWithEnv(cfg, "nats", "url") - if natsUrl == "" { - natsUrl = nats.DefaultURL - } - - events, err := events.NewAsyncEvents(stopCtx, natsUrl) - if err != nil { - logger.Fatal("Could not create async events client: ", err) - } - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - if err := events.Close(ctx); err != nil { - logger.Printf("Error closing events handler: %s", err) - } - }() - - dnsMonitor, err := dns.NewMonitor(logger, dnsMonitorInterval, nil) - if err != nil { - logger.Fatal("Could not create DNS monitor: ", err) - } - if err := dnsMonitor.Start(); err != nil { - logger.Fatal("Could not start DNS monitor: ", err) - } - defer dnsMonitor.Stop() - - etcdClient, err := etcd.NewClient(logger, cfg, "mcu") - if err != nil { - logger.Fatalf("Could not create etcd client: %s", err) - } - defer func() { - if err := etcdClient.Close(); err != nil { - logger.Printf("Error while closing etcd client: %s", err) - } - }() - - rpcServer, err := grpc.NewServer(stopCtx, cfg, version) - if err != nil { - logger.Fatalf("Could not create RPC server: %s", err) - } - go func() { - if err := rpcServer.Run(); err != nil { - logger.Fatalf("Could not start RPC server: %s", err) - } - }() - defer rpcServer.Close() - - rpcClients, err := grpc.NewClients(stopCtx, cfg, etcdClient, dnsMonitor, version) - if err != nil { - logger.Fatalf("Could not create RPC clients: %s", err) - } - defer rpcClients.Close() - - r := mux.NewRouter() - hub, err := server.NewHub(stopCtx, cfg, events, rpcServer, rpcClients, etcdClient, r, version) - if err != nil { - logger.Fatal("Could not create hub: ", err) - } - - mcuUrl, _ := config.GetStringOptionWithEnv(cfg, "mcu", "url") - mcuType, _ := cfg.GetString("mcu", "type") - if mcuType == "" && mcuUrl != "" { - logger.Printf("WARNING: Old-style MCU configuration detected with url but no type, defaulting to type %s", sfu.TypeJanus) - mcuType = sfu.TypeJanus - } else if mcuType == sfu.TypeJanus && mcuUrl == "" { - logger.Printf("WARNING: Old-style MCU configuration detected with type but no url, disabling") - mcuType = "" - } - - if mcuType != "" { - var mcu sfu.SFU - mcuRetry := initialMcuRetry - mcuRetryTimer := time.NewTimer(mcuRetry) - mcuTypeLoop: - for { - // Context should be cancelled on signals but need a way to differentiate later. - ctx := context.TODO() - switch mcuType { - case sfu.TypeJanus: - mcu, err = janus.NewJanusSFU(ctx, mcuUrl, cfg) - proxy.UnregisterStats() - janus.RegisterStats() - case sfu.TypeProxy: - mcu, err = proxy.NewProxySFU(ctx, cfg, etcdClient, rpcClients, dnsMonitor) - janus.UnregisterStats() - proxy.RegisterStats() - default: - logger.Fatal("Unsupported MCU type: ", mcuType) - } - if err == nil { - err = mcu.Start(ctx) - if err != nil { - logger.Printf("Could not create %s MCU: %s", mcuType, err) - } - } - if err == nil { - break - } - - logger.Printf("Could not initialize %s MCU (%s) will retry in %s", mcuType, err, mcuRetry) - mcuRetryTimer.Reset(mcuRetry) - select { - case <-stopCtx.Done(): - logger.Fatalf("Cancelled") - case sig := <-sigChan: - switch sig { - case syscall.SIGHUP: - logger.Printf("Received SIGHUP, reloading %s", *configFlag) - if cfg, err = goconf.ReadConfigFile(*configFlag); err != nil { - logger.Printf("Could not read configuration from %s: %s", *configFlag, err) - } else { - mcuUrl, _ = config.GetStringOptionWithEnv(cfg, "mcu", "url") - mcuType, _ = cfg.GetString("mcu", "type") - if mcuType == "" && mcuUrl != "" { - logger.Printf("WARNING: Old-style MCU configuration detected with url but no type, defaulting to type %s", sfu.TypeJanus) - mcuType = sfu.TypeJanus - } else if mcuType == sfu.TypeJanus && mcuUrl == "" { - logger.Printf("WARNING: Old-style MCU configuration detected with type but no url, disabling") - mcuType = "" - break mcuTypeLoop - } - } - } - case <-mcuRetryTimer.C: - // Retry connection - mcuRetry = min(mcuRetry*2, maxMcuRetry) - } - } - if mcu != nil { - defer mcu.Stop() - - logger.Printf("Using %s MCU", mcuType) - hub.SetMcu(mcu) - } - } - - go hub.Run() - defer hub.Stop() - - server, err := server.NewBackendServer(stopCtx, cfg, hub, version) - if err != nil { - logger.Fatal("Could not create backend server: ", err) - } - if err := server.Start(r); err != nil { - logger.Fatal("Could not start backend server: ", err) - } - - listeners := Listeners{ - logger: logger, - } - - if saddr, _ := config.GetStringOptionWithEnv(cfg, "https", "listen"); saddr != "" { - cert, _ := cfg.GetString("https", "certificate") - key, _ := cfg.GetString("https", "key") - if cert == "" || key == "" { - logger.Fatal("Need a certificate and key for the HTTPS listener") - } - - readTimeout, _ := cfg.GetInt("https", "readtimeout") - if readTimeout <= 0 { - readTimeout = defaultReadTimeout - } - writeTimeout, _ := cfg.GetInt("https", "writetimeout") - if writeTimeout <= 0 { - writeTimeout = defaultWriteTimeout - } - for address := range internal.SplitEntries(saddr, " ") { - go func(address string) { - logger.Println("Listening on", address) - listener, err := createTLSListener(address, cert, key) - if err != nil { - logger.Fatal("Could not start listening: ", err) - } - srv := &http.Server{ - Handler: r, - - ReadTimeout: time.Duration(readTimeout) * time.Second, - WriteTimeout: time.Duration(writeTimeout) * time.Second, - } - listeners.Add(listener) - if err := srv.Serve(listener); err != nil { - if !hub.IsShutdownScheduled() || !errors.Is(err, net.ErrClosed) { - logger.Fatal("Could not start server: ", err) - } - } - }(address) - } - } - - if addr, _ := config.GetStringOptionWithEnv(cfg, "http", "listen"); addr != "" { - readTimeout, _ := cfg.GetInt("http", "readtimeout") - if readTimeout <= 0 { - readTimeout = defaultReadTimeout - } - writeTimeout, _ := cfg.GetInt("http", "writetimeout") - if writeTimeout <= 0 { - writeTimeout = defaultWriteTimeout - } - - for address := range internal.SplitEntries(addr, " ") { - go func(address string) { - logger.Println("Listening on", address) - listener, err := createListener(address) - if err != nil { - logger.Fatal("Could not start listening: ", err) - } - srv := &http.Server{ - Handler: r, - Addr: addr, - - ReadTimeout: time.Duration(readTimeout) * time.Second, - WriteTimeout: time.Duration(writeTimeout) * time.Second, - } - listeners.Add(listener) - if err := srv.Serve(listener); err != nil { - if !hub.IsShutdownScheduled() || !errors.Is(err, net.ErrClosed) { - logger.Fatal("Could not start server: ", err) - } - } - }(address) - } - } - -loop: - for { - select { - case <-stopCtx.Done(): - logger.Println("Interrupted") - break loop - case sig := <-sigChan: - switch sig { - case syscall.SIGHUP: - logger.Printf("Received SIGHUP, reloading %s", *configFlag) - if config, err := goconf.ReadConfigFile(*configFlag); err != nil { - logger.Printf("Could not read configuration from %s: %s", *configFlag, err) - } else { - hub.Reload(stopCtx, config) - server.Reload(config) - } - case syscall.SIGUSR1: - logger.Printf("Received SIGUSR1, scheduling server to shutdown") - hub.ScheduleShutdown() - listeners.Close() - } - case <-hub.ShutdownChannel(): - logger.Printf("All clients disconnected, shutting down") - break loop - } - } -} diff --git a/container/concurrentmap.go b/concurrentmap.go similarity index 65% rename from container/concurrentmap.go rename to concurrentmap.go index 8170446..1a4da0d 100644 --- a/container/concurrentmap.go +++ b/concurrentmap.go @@ -19,48 +19,47 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package container +package signaling import ( "sync" ) -type ConcurrentMap[K comparable, V any] struct { - mu sync.RWMutex - // +checklocks:mu - d map[K]V +type ConcurrentStringStringMap struct { + sync.Mutex + d map[string]string } -func (m *ConcurrentMap[K, V]) Set(key K, value V) { - m.mu.Lock() - defer m.mu.Unlock() +func (m *ConcurrentStringStringMap) Set(key, value string) { + m.Lock() + defer m.Unlock() if m.d == nil { - m.d = make(map[K]V) + m.d = make(map[string]string) } m.d[key] = value } -func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) { - m.mu.RLock() - defer m.mu.RUnlock() +func (m *ConcurrentStringStringMap) Get(key string) (string, bool) { + m.Lock() + defer m.Unlock() s, found := m.d[key] return s, found } -func (m *ConcurrentMap[K, V]) Del(key K) { - m.mu.Lock() - defer m.mu.Unlock() +func (m *ConcurrentStringStringMap) Del(key string) { + m.Lock() + defer m.Unlock() delete(m.d, key) } -func (m *ConcurrentMap[K, V]) Len() int { - m.mu.RLock() - defer m.mu.RUnlock() +func (m *ConcurrentStringStringMap) Len() int { + m.Lock() + defer m.Unlock() return len(m.d) } -func (m *ConcurrentMap[K, V]) Clear() { - m.mu.Lock() - defer m.mu.Unlock() +func (m *ConcurrentStringStringMap) Clear() { + m.Lock() + defer m.Unlock() m.d = nil } diff --git a/container/concurrentmap_test.go b/concurrentmap_test.go similarity index 83% rename from container/concurrentmap_test.go rename to concurrentmap_test.go index 98b126d..cca1d29 100644 --- a/container/concurrentmap_test.go +++ b/concurrentmap_test.go @@ -19,10 +19,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package container +package signaling import ( - "crypto/rand" "strconv" "sync" "testing" @@ -31,9 +30,8 @@ import ( ) func TestConcurrentStringStringMap(t *testing.T) { - t.Parallel() assert := assert.New(t) - var m ConcurrentMap[string, string] + var m ConcurrentStringStringMap assert.Equal(0, m.Len()) v, found := m.Get("foo") assert.False(found, "Expected missing entry, got %s", v) @@ -78,22 +76,21 @@ func TestConcurrentStringStringMap(t *testing.T) { var wg sync.WaitGroup concurrency := 100 count := 1000 - for x := range concurrency { - wg.Go(func() { + for x := 0; x < concurrency; x++ { + wg.Add(1) + go func(x int) { + defer wg.Done() + key := "key-" + strconv.Itoa(x) - rnd := rand.Text() - for y := range count { - value := rnd + "-" + strconv.Itoa(y) + for y := 0; y < count; y = y + 1 { + value := newRandomString(32) m.Set(key, value) - if v, found := m.Get(key); !found { - assert.True(found, "Expected entry for key %s", key) - return - } else if v != value { - assert.Equal(value, v, "Unexpected value for key %s", key) + if v, found := m.Get(key); !assert.True(found, "Expected entry for key %s", key) || + !assert.Equal(value, v, "Unexpected value for key %s", key) { return } } - }) + }(x) } wg.Wait() assert.Equal(concurrency, m.Len()) diff --git a/config/config.go b/config.go similarity index 92% rename from config/config.go rename to config.go index 719cc67..c3a006e 100644 --- a/config/config.go +++ b/config.go @@ -19,15 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package config +package signaling import ( + "errors" "os" "regexp" "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" ) var ( @@ -72,7 +71,8 @@ func GetStringOptions(config *goconf.ConfigFile, section string, ignoreErrors bo continue } - if ge, ok := internal.AsErrorType[goconf.GetError](err); ok && ge.Reason == goconf.OptionNotFound { + var ge goconf.GetError + if errors.As(err, &ge) && ge.Reason == goconf.OptionNotFound { // Skip options from "default" section. continue } diff --git a/config/config_test.go b/config_test.go similarity index 99% rename from config/config_test.go rename to config_test.go index 7727138..f0cc619 100644 --- a/config/config_test.go +++ b/config_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package config +package signaling import ( "testing" diff --git a/geoip/continentmap.go b/continentmap.go similarity index 98% rename from geoip/continentmap.go rename to continentmap.go index 75673bd..a99b944 100644 --- a/geoip/continentmap.go +++ b/continentmap.go @@ -1,10 +1,10 @@ -package geoip +package signaling // This file has been automatically generated, do not modify. // Source: https://raw.githubusercontent.com/datasets/country-codes/refs/heads/main/data/country-codes.csv var ( - ContinentMap = map[Country][]Continent{ + ContinentMap = map[string][]string{ "AD": {"EU"}, "AE": {"AS"}, "AF": {"AS"}, diff --git a/async/deferred_executor.go b/deferred_executor.go similarity index 81% rename from async/deferred_executor.go rename to deferred_executor.go index be04695..a6f46c7 100644 --- a/async/deferred_executor.go +++ b/deferred_executor.go @@ -19,32 +19,29 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package async +package signaling import ( + "log" "reflect" "runtime" "runtime/debug" "sync" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) // DeferredExecutor will asynchronously execute functions while maintaining // their order. type DeferredExecutor struct { - logger log.Logger queue chan func() closed chan struct{} closeOnce sync.Once } -func NewDeferredExecutor(logger log.Logger, queueSize int) *DeferredExecutor { +func NewDeferredExecutor(queueSize int) *DeferredExecutor { if queueSize < 0 { queueSize = 0 } result := &DeferredExecutor{ - logger: logger, queue: make(chan func(), queueSize), closed: make(chan struct{}), } @@ -65,15 +62,15 @@ func (e *DeferredExecutor) run() { } } -func getFunctionName(i any) string { +func getFunctionName(i interface{}) string { return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() } func (e *DeferredExecutor) Execute(f func()) { defer func() { - if err := recover(); err != nil { - e.logger.Printf("Could not defer function %v: %+v", getFunctionName(f), err) - e.logger.Printf("Called from %s", string(debug.Stack())) + if e := recover(); e != nil { + log.Printf("Could not defer function %v: %+v", getFunctionName(f), e) + log.Printf("Called from %s", string(debug.Stack())) } }() diff --git a/async/deferred_executor_test.go b/deferred_executor_test.go similarity index 65% rename from async/deferred_executor_test.go rename to deferred_executor_test.go index 7ebf8b8..8fa96ce 100644 --- a/async/deferred_executor_test.go +++ b/deferred_executor_test.go @@ -19,22 +19,17 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package async +package signaling import ( "testing" - "testing/synctest" "time" "github.com/stretchr/testify/assert" - - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" ) func TestDeferredExecutor_MultiClose(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - e := NewDeferredExecutor(logger, 0) + e := NewDeferredExecutor(0) defer e.waitForStop() e.Close() @@ -43,32 +38,28 @@ func TestDeferredExecutor_MultiClose(t *testing.T) { func TestDeferredExecutor_QueueSize(t *testing.T) { t.Parallel() - synctest.Test(t, func(t *testing.T) { - logger := logtest.NewLoggerForTest(t) - e := NewDeferredExecutor(logger, 0) - defer e.waitForStop() - defer e.Close() + e := NewDeferredExecutor(0) + defer e.waitForStop() + defer e.Close() - delay := 100 * time.Millisecond - e.Execute(func() { - time.Sleep(delay) - }) - - // The queue will block until the first command finishes. - a := time.Now() - e.Execute(func() { - time.Sleep(time.Millisecond) - }) - b := time.Now() - delta := b.Sub(a) - assert.Equal(t, delay, delta) + delay := 100 * time.Millisecond + e.Execute(func() { + time.Sleep(delay) }) + + // The queue will block until the first command finishes. + a := time.Now() + e.Execute(func() { + time.Sleep(time.Millisecond) + }) + b := time.Now() + delta := b.Sub(a) + // Allow one millisecond less delay to account for time variance on CI runners. + assert.GreaterOrEqual(t, delta+time.Millisecond, delay) } func TestDeferredExecutor_Order(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - e := NewDeferredExecutor(logger, 64) + e := NewDeferredExecutor(64) defer e.waitForStop() defer e.Close() @@ -80,7 +71,7 @@ func TestDeferredExecutor_Order(t *testing.T) { } done := make(chan struct{}) - for x := range 10 { + for x := 0; x < 10; x++ { e.Execute(getFunc(x)) } @@ -89,15 +80,13 @@ func TestDeferredExecutor_Order(t *testing.T) { }) <-done - for x := range 10 { + for x := 0; x < 10; x++ { assert.Equal(t, entries[x], x, "Unexpected at position %d", x) } } func TestDeferredExecutor_CloseFromFunc(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - e := NewDeferredExecutor(logger, 64) + e := NewDeferredExecutor(64) defer e.waitForStop() done := make(chan struct{}) @@ -110,9 +99,8 @@ func TestDeferredExecutor_CloseFromFunc(t *testing.T) { } func TestDeferredExecutor_DeferAfterClose(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - e := NewDeferredExecutor(logger, 64) + CatchLogForTest(t) + e := NewDeferredExecutor(64) defer e.waitForStop() e.Close() @@ -123,9 +111,7 @@ func TestDeferredExecutor_DeferAfterClose(t *testing.T) { } func TestDeferredExecutor_WaitForStopTwice(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - e := NewDeferredExecutor(logger, 64) + e := NewDeferredExecutor(64) defer e.waitForStop() e.Close() diff --git a/dist/init/systemd/signaling.service b/dist/init/systemd/signaling.service index 21d75c3..cdf67fa 100644 --- a/dist/init/systemd/signaling.service +++ b/dist/init/systemd/signaling.service @@ -12,7 +12,7 @@ ConfigurationDirectory=signaling # Hardening - see systemd.exec(5) CapabilityBoundingSet= -ExecPaths=/usr/bin/signaling /usr/lib /usr/lib64 +ExecPaths=/usr/bin/signaling /usr/lib LockPersonality=yes MemoryDenyWriteExecute=yes NoExecPaths=/ diff --git a/dns/internal/mock_lookup.go b/dns/internal/mock_lookup.go deleted file mode 100644 index be7fcdf..0000000 --- a/dns/internal/mock_lookup.go +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "net" - "sync" -) - -type MockLookup struct { - sync.RWMutex - - // +checklocks:RWMutex - ips map[string][]net.IP -} - -func NewMockLookup() *MockLookup { - mock := &MockLookup{ - ips: make(map[string][]net.IP), - } - return mock -} - -func (m *MockLookup) Set(host string, ips []net.IP) { - m.Lock() - defer m.Unlock() - - m.ips[host] = ips -} - -func (m *MockLookup) Get(host string) []net.IP { - m.Lock() - defer m.Unlock() - - return m.ips[host] -} - -func (m *MockLookup) Lookup(host string) ([]net.IP, error) { - m.RLock() - defer m.RUnlock() - - ips, found := m.ips[host] - if !found { - return nil, &net.DNSError{ - Err: "could not resolve " + host, - Name: host, - IsNotFound: true, - } - } - - return append([]net.IP{}, ips...), nil -} diff --git a/dns/test/dns_test.go b/dns/test/dns_test.go deleted file mode 100644 index c743c02..0000000 --- a/dns/test/dns_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "net" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" -) - -func TestDnsMonitor(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - lookup := NewMockLookup() - ips := []net.IP{ - net.ParseIP("1.2.3.4"), - } - lookup.Set("domain.invalid", ips) - monitor := NewMonitorForTest(t, time.Second, lookup) - - called := make(chan struct{}) - callback1 := func(entry *dns.MonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) { - defer func() { - called <- struct{}{} - }() - - assert.Equal(ips, all) - assert.Equal(ips, add) - assert.Empty(keep) - assert.Empty(remove) - } - - entry1, err := monitor.Add("domain.invalid", callback1) - require.NoError(err) - - t.Cleanup(func() { - monitor.Remove(entry1) - }) - - <-called -} diff --git a/dns/monitor.go b/dns_monitor.go similarity index 65% rename from dns/monitor.go rename to dns_monitor.go index 05e458c..072e01c 100644 --- a/dns/monitor.go +++ b/dns_monitor.go @@ -19,64 +19,49 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package dns +package signaling import ( "context" + "log" "net" "net/url" - "slices" "strings" "sync" "sync/atomic" "time" +) - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" +var ( + lookupDnsMonitorIP = net.LookupIP ) const ( - defaultMonitorInterval = time.Second + defaultDnsMonitorInterval = time.Second ) -type MonitorCallback = func(entry *MonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) +type DnsMonitorCallback = func(entry *DnsMonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) -type MonitorEntry struct { - entry atomic.Pointer[monitorEntry] +type DnsMonitorEntry struct { + entry atomic.Pointer[dnsMonitorEntry] url string - callback MonitorCallback + callback DnsMonitorCallback } -func (e *MonitorEntry) URL() string { +func (e *DnsMonitorEntry) URL() string { return e.url } -type monitorEntry struct { +type dnsMonitorEntry struct { hostname string hostIP net.IP - mu sync.Mutex - // +checklocks:mu - ips []net.IP - // +checklocks:mu - entries map[*MonitorEntry]bool + mu sync.Mutex + ips []net.IP + entries map[*DnsMonitorEntry]bool } -func (e *monitorEntry) clearRemoved() bool { - e.mu.Lock() - defer e.mu.Unlock() - - deleted := false - for entry := range e.entries { - if entry.entry.Load() == nil { - delete(e.entries, entry) - deleted = true - } - } - - return deleted && len(e.entries) == 0 -} - -func (e *monitorEntry) setIPs(ips []net.IP, fromIP bool) { +func (e *dnsMonitorEntry) setIPs(ips []net.IP, fromIP bool) { e.mu.Lock() defer e.mu.Unlock() @@ -109,7 +94,7 @@ func (e *monitorEntry) setIPs(ips []net.IP, fromIP bool) { found := false for idx, newIP := range ips { if oldIP.Equal(newIP) { - ips = slices.Delete(ips, idx, idx+1) + ips = append(ips[:idx], ips[idx+1:]...) found = true keepIPs = append(keepIPs, oldIP) newIPs = append(newIPs, oldIP) @@ -133,14 +118,14 @@ func (e *monitorEntry) setIPs(ips []net.IP, fromIP bool) { } } -func (e *monitorEntry) addEntry(entry *MonitorEntry) { +func (e *dnsMonitorEntry) addEntry(entry *DnsMonitorEntry) { e.mu.Lock() defer e.mu.Unlock() e.entries[entry] = true } -func (e *monitorEntry) removeEntry(entry *MonitorEntry) bool { +func (e *dnsMonitorEntry) removeEntry(entry *DnsMonitorEntry) bool { e.mu.Lock() defer e.mu.Unlock() @@ -148,19 +133,14 @@ func (e *monitorEntry) removeEntry(entry *MonitorEntry) bool { return len(e.entries) == 0 } -// +checklocks:e.mu -func (e *monitorEntry) runCallbacks(all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) { +func (e *dnsMonitorEntry) runCallbacks(all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) { for entry := range e.entries { entry.callback(entry, all, add, keep, remove) } } -type MonitorLookupFunc func(hostname string) ([]net.IP, error) - -type Monitor struct { - logger log.Logger - interval time.Duration - lookupFunc MonitorLookupFunc +type DnsMonitor struct { + interval time.Duration stopCtx context.Context stopFunc func() @@ -168,48 +148,46 @@ type Monitor struct { mu sync.RWMutex cond *sync.Cond - hostnames map[string]*monitorEntry + hostnames map[string]*dnsMonitorEntry - tickerWaiting atomic.Bool - hasRemoved atomic.Bool + hasRemoved atomic.Bool + + // Can be overwritten from tests. + checkHostnames func() } -func NewMonitor(logger log.Logger, interval time.Duration, lookupFunc MonitorLookupFunc) (*Monitor, error) { +func NewDnsMonitor(interval time.Duration) (*DnsMonitor, error) { if interval < 0 { - interval = defaultMonitorInterval - } - if lookupFunc == nil { - lookupFunc = net.LookupIP + interval = defaultDnsMonitorInterval } stopCtx, stopFunc := context.WithCancel(context.Background()) - monitor := &Monitor{ - logger: logger, - interval: interval, - lookupFunc: lookupFunc, + monitor := &DnsMonitor{ + interval: interval, stopCtx: stopCtx, stopFunc: stopFunc, stopped: make(chan struct{}), - hostnames: make(map[string]*monitorEntry), + hostnames: make(map[string]*dnsMonitorEntry), } monitor.cond = sync.NewCond(&monitor.mu) + monitor.checkHostnames = monitor.doCheckHostnames return monitor, nil } -func (m *Monitor) Start() error { +func (m *DnsMonitor) Start() error { go m.run() return nil } -func (m *Monitor) Stop() { +func (m *DnsMonitor) Stop() { m.stopFunc() m.cond.Signal() <-m.stopped } -func (m *Monitor) Add(target string, callback MonitorCallback) (*MonitorEntry, error) { +func (m *DnsMonitor) Add(target string, callback DnsMonitorCallback) (*DnsMonitorEntry, error) { var hostname string if strings.Contains(target, "://") { // Full URL passed. @@ -229,17 +207,17 @@ func (m *Monitor) Add(target string, callback MonitorCallback) (*MonitorEntry, e m.mu.Lock() defer m.mu.Unlock() - e := &MonitorEntry{ + e := &DnsMonitorEntry{ url: target, callback: callback, } entry, found := m.hostnames[hostname] if !found { - entry = &monitorEntry{ + entry = &dnsMonitorEntry{ hostname: hostname, hostIP: net.ParseIP(hostname), - entries: make(map[*MonitorEntry]bool), + entries: make(map[*DnsMonitorEntry]bool), } m.hostnames[hostname] = entry } @@ -249,7 +227,7 @@ func (m *Monitor) Add(target string, callback MonitorCallback) (*MonitorEntry, e return e, nil } -func (m *Monitor) Remove(entry *MonitorEntry) { +func (m *DnsMonitor) Remove(entry *DnsMonitorEntry) { oldEntry := entry.entry.Swap(nil) if oldEntry == nil { // Already removed. @@ -267,7 +245,7 @@ func (m *Monitor) Remove(entry *MonitorEntry) { m.hasRemoved.Store(true) return } - defer m.mu.Unlock() // +checklocksforce: only executed if the TryLock above succeeded. + defer m.mu.Unlock() e, found := m.hostnames[oldEntry.hostname] if !found { @@ -279,7 +257,7 @@ func (m *Monitor) Remove(entry *MonitorEntry) { } } -func (m *Monitor) clearRemoved() { +func (m *DnsMonitor) clearRemoved() { if !m.hasRemoved.CompareAndSwap(true, false) { return } @@ -288,13 +266,21 @@ func (m *Monitor) clearRemoved() { defer m.mu.Unlock() for hostname, entry := range m.hostnames { - if entry.clearRemoved() { + deleted := false + for e := range entry.entries { + if e.entry.Load() == nil { + delete(entry.entries, e) + deleted = true + } + } + + if deleted && len(entry.entries) == 0 { delete(m.hostnames, hostname) } } } -func (m *Monitor) waitForEntries() (waited bool) { +func (m *DnsMonitor) waitForEntries() (waited bool) { m.mu.Lock() defer m.mu.Unlock() @@ -305,7 +291,7 @@ func (m *Monitor) waitForEntries() (waited bool) { return } -func (m *Monitor) run() { +func (m *DnsMonitor) run() { ticker := time.NewTicker(m.interval) defer ticker.Stop() defer close(m.stopped) @@ -316,22 +302,21 @@ func (m *Monitor) run() { if m.stopCtx.Err() == nil { // Initial check when a new entry was added. More checks will be // triggered by the Ticker. - m.CheckHostnames() + m.checkHostnames() continue } } - m.tickerWaiting.Store(true) select { case <-m.stopCtx.Done(): return case <-ticker.C: - m.CheckHostnames() + m.checkHostnames() } } } -func (m *Monitor) CheckHostnames() { +func (m *DnsMonitor) doCheckHostnames() { m.clearRemoved() m.mu.RLock() @@ -342,27 +327,17 @@ func (m *Monitor) CheckHostnames() { } } -func (m *Monitor) checkHostname(entry *monitorEntry) { +func (m *DnsMonitor) checkHostname(entry *dnsMonitorEntry) { if len(entry.hostIP) > 0 { entry.setIPs([]net.IP{entry.hostIP}, true) return } - ips, err := m.lookupFunc(entry.hostname) + ips, err := lookupDnsMonitorIP(entry.hostname) if err != nil { - m.logger.Printf("Could not lookup %s: %s", entry.hostname, err) + log.Printf("Could not lookup %s: %s", entry.hostname, err) return } entry.setIPs(ips, false) } - -func (m *Monitor) WaitForTicker(ctx context.Context) error { - for !m.tickerWaiting.Load() { - time.Sleep(time.Millisecond) - if err := ctx.Err(); err != nil { - return err - } - } - return nil -} diff --git a/dns/monitor_test.go b/dns_monitor_test.go similarity index 65% rename from dns/monitor_test.go rename to dns_monitor_test.go index d92868d..ee10772 100644 --- a/dns/monitor_test.go +++ b/dns_monitor_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package dns +package signaling import ( "context" @@ -33,20 +33,61 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns/internal" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" ) -func NewMonitorForTest(t *testing.T, interval time.Duration, lookup *internal.MockLookup) *Monitor { +type mockDnsLookup struct { + sync.RWMutex + + ips map[string][]net.IP +} + +func newMockDnsLookupForTest(t *testing.T) *mockDnsLookup { + mock := &mockDnsLookup{ + ips: make(map[string][]net.IP), + } + prev := lookupDnsMonitorIP + t.Cleanup(func() { + lookupDnsMonitorIP = prev + }) + lookupDnsMonitorIP = mock.lookup + return mock +} + +func (m *mockDnsLookup) Set(host string, ips []net.IP) { + m.Lock() + defer m.Unlock() + + m.ips[host] = ips +} + +func (m *mockDnsLookup) Get(host string) []net.IP { + m.Lock() + defer m.Unlock() + + return m.ips[host] +} + +func (m *mockDnsLookup) lookup(host string) ([]net.IP, error) { + m.RLock() + defer m.RUnlock() + + ips, found := m.ips[host] + if !found { + return nil, &net.DNSError{ + Err: fmt.Sprintf("could not resolve %s", host), + Name: host, + IsNotFound: true, + } + } + + return append([]net.IP{}, ips...), nil +} + +func newDnsMonitorForTest(t *testing.T, interval time.Duration) *DnsMonitor { t.Helper() require := require.New(t) - logger := logtest.NewLoggerForTest(t) - var lookupFunc MonitorLookupFunc - if lookup != nil { - lookupFunc = lookup.Lookup - } - monitor, err := NewMonitor(logger, interval, lookupFunc) + monitor, err := NewDnsMonitor(interval) require.NoError(err) t.Cleanup(func() { @@ -57,48 +98,46 @@ func NewMonitorForTest(t *testing.T, interval time.Duration, lookup *internal.Mo return monitor } -type monitorReceiverRecord struct { +type dnsMonitorReceiverRecord struct { all []net.IP add []net.IP keep []net.IP remove []net.IP } -func (r *monitorReceiverRecord) Equal(other *monitorReceiverRecord) bool { +func (r *dnsMonitorReceiverRecord) Equal(other *dnsMonitorReceiverRecord) bool { return r == other || (reflect.DeepEqual(r.add, other.add) && reflect.DeepEqual(r.keep, other.keep) && reflect.DeepEqual(r.remove, other.remove)) } -func (r *monitorReceiverRecord) String() string { +func (r *dnsMonitorReceiverRecord) String() string { return fmt.Sprintf("all=%v, add=%v, keep=%v, remove=%v", r.all, r.add, r.keep, r.remove) } var ( - expectNone = &monitorReceiverRecord{} // +checklocksignore: Global readonly variable. + expectNone = &dnsMonitorReceiverRecord{} ) -type monitorReceiver struct { +type dnsMonitorReceiver struct { sync.Mutex - t *testing.T - // +checklocks:Mutex - expected *monitorReceiverRecord - // +checklocks:Mutex - received *monitorReceiverRecord + t *testing.T + expected *dnsMonitorReceiverRecord + received *dnsMonitorReceiverRecord } -func newMonitorReceiverForTest(t *testing.T) *monitorReceiver { - return &monitorReceiver{ +func newDnsMonitorReceiverForTest(t *testing.T) *dnsMonitorReceiver { + return &dnsMonitorReceiver{ t: t, } } -func (r *monitorReceiver) OnLookup(entry *MonitorEntry, all, add, keep, remove []net.IP) { +func (r *dnsMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all, add, keep, remove []net.IP) { r.Lock() defer r.Unlock() - received := &monitorReceiverRecord{ + received := &dnsMonitorReceiverRecord{ all: all, add: add, keep: keep, @@ -108,13 +147,13 @@ func (r *monitorReceiver) OnLookup(entry *MonitorEntry, all, add, keep, remove [ expected := r.expected r.expected = nil if expected == expectNone { - assert.Fail(r.t, "expected no event", "received %v", received) + assert.Fail(r.t, "expected no event, got %v", received) return } if expected == nil { if r.received != nil && !r.received.Equal(received) { - assert.Fail(r.t, "unexpected message", "already received %v, got %v", r.received, received) + assert.Fail(r.t, "already received %v, got %v", r.received, received) } return } @@ -124,7 +163,7 @@ func (r *monitorReceiver) OnLookup(entry *MonitorEntry, all, add, keep, remove [ r.expected = nil } -func (r *monitorReceiver) WaitForExpected(ctx context.Context) { +func (r *dnsMonitorReceiver) WaitForExpected(ctx context.Context) { r.t.Helper() r.Lock() defer r.Unlock() @@ -143,16 +182,16 @@ func (r *monitorReceiver) WaitForExpected(ctx context.Context) { } } -func (r *monitorReceiver) Expect(all, add, keep, remove []net.IP) { +func (r *dnsMonitorReceiver) Expect(all, add, keep, remove []net.IP) { r.t.Helper() r.Lock() defer r.Unlock() if r.expected != nil && r.expected != expectNone { - assert.Fail(r.t, "didn't get previous message", "expected %v", r.expected) + assert.Fail(r.t, "didn't get previously expected %v", r.expected) } - expected := &monitorReceiverRecord{ + expected := &dnsMonitorReceiverRecord{ all: all, add: add, keep: keep, @@ -166,26 +205,25 @@ func (r *monitorReceiver) Expect(all, add, keep, remove []net.IP) { r.expected = expected } -func (r *monitorReceiver) ExpectNone() { +func (r *dnsMonitorReceiver) ExpectNone() { r.t.Helper() r.Lock() defer r.Unlock() if r.expected != nil && r.expected != expectNone { - assert.Fail(r.t, "didn't get previous message", "expected %v", r.expected) + assert.Fail(r.t, "didn't get previously expected %v", r.expected) } r.expected = expectNone } -func TestMonitor(t *testing.T) { - t.Parallel() - lookup := internal.NewMockLookup() +func TestDnsMonitor(t *testing.T) { + lookup := newMockDnsLookupForTest(t) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() interval := time.Millisecond - monitor := NewMonitorForTest(t, interval, lookup) + monitor := newDnsMonitorForTest(t, interval) ip1 := net.ParseIP("192.168.0.1") ip2 := net.ParseIP("192.168.1.1") @@ -196,7 +234,7 @@ func TestMonitor(t *testing.T) { } lookup.Set("foo", ips1) - rec1 := newMonitorReceiverForTest(t) + rec1 := newDnsMonitorReceiverForTest(t) rec1.Expect(ips1, ips1, nil, nil) entry1, err := monitor.Add("https://foo:12345", rec1.OnLookup) @@ -247,20 +285,19 @@ func TestMonitor(t *testing.T) { time.Sleep(5 * interval) } -func TestMonitorIP(t *testing.T) { - t.Parallel() +func TestDnsMonitorIP(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() interval := time.Millisecond - monitor := NewMonitorForTest(t, interval, nil) + monitor := newDnsMonitorForTest(t, interval) ip := "192.168.0.1" ips := []net.IP{ net.ParseIP(ip), } - rec1 := newMonitorReceiverForTest(t) + rec1 := newDnsMonitorReceiverForTest(t) rec1.Expect(ips, ips, nil, nil) entry, err := monitor.Add(ip+":12345", rec1.OnLookup) @@ -273,15 +310,14 @@ func TestMonitorIP(t *testing.T) { time.Sleep(5 * interval) } -func TestMonitorNoLookupIfEmpty(t *testing.T) { - t.Parallel() +func TestDnsMonitorNoLookupIfEmpty(t *testing.T) { interval := time.Millisecond - monitor := NewMonitorForTest(t, interval, nil) + monitor := newDnsMonitorForTest(t, interval) var checked atomic.Bool - monitor.lookupFunc = func(hostname string) ([]net.IP, error) { + monitor.checkHostnames = func() { checked.Store(true) - return net.LookupIP(hostname) + monitor.doCheckHostnames() } time.Sleep(10 * interval) @@ -290,20 +326,18 @@ func TestMonitorNoLookupIfEmpty(t *testing.T) { type deadlockMonitorReceiver struct { t *testing.T - monitor *Monitor // +checklocksignore: Only written to from constructor. + monitor *DnsMonitor mu sync.RWMutex - wg sync.WaitGroup // +checklocksignore: Only written to from constructor. + wg sync.WaitGroup - // +checklocks:mu - entry *MonitorEntry - started chan struct{} - // +checklocks:mu + entry *DnsMonitorEntry + started chan struct{} triggered bool closed atomic.Bool } -func newDeadlockMonitorReceiver(t *testing.T, monitor *Monitor) *deadlockMonitorReceiver { +func newDeadlockMonitorReceiver(t *testing.T, monitor *DnsMonitor) *deadlockMonitorReceiver { return &deadlockMonitorReceiver{ t: t, monitor: monitor, @@ -311,7 +345,7 @@ func newDeadlockMonitorReceiver(t *testing.T, monitor *Monitor) *deadlockMonitor } } -func (r *deadlockMonitorReceiver) OnLookup(entry *MonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) { +func (r *deadlockMonitorReceiver) OnLookup(entry *DnsMonitorEntry, all []net.IP, add []net.IP, keep []net.IP, remove []net.IP) { if !assert.False(r.t, r.closed.Load(), "received lookup after closed") { return } @@ -324,13 +358,16 @@ func (r *deadlockMonitorReceiver) OnLookup(entry *MonitorEntry, all []net.IP, ad } r.triggered = true - r.wg.Go(func() { + r.wg.Add(1) + go func() { + defer r.wg.Done() + r.mu.RLock() defer r.mu.RUnlock() close(r.started) time.Sleep(50 * time.Millisecond) - }) + }() } func (r *deadlockMonitorReceiver) Start() { @@ -356,15 +393,14 @@ func (r *deadlockMonitorReceiver) Close() { r.wg.Wait() } -func TestMonitorDeadlock(t *testing.T) { - t.Parallel() - lookup := internal.NewMockLookup() +func TestDnsMonitorDeadlock(t *testing.T) { + lookup := newMockDnsLookupForTest(t) ip1 := net.ParseIP("192.168.0.1") ip2 := net.ParseIP("192.168.0.2") lookup.Set("foo", []net.IP{ip1}) interval := time.Millisecond - monitor := NewMonitorForTest(t, interval, lookup) + monitor := newDnsMonitorForTest(t, interval) r := newDeadlockMonitorReceiver(t, monitor) r.Start() diff --git a/docker/README.md b/docker/README.md index 83760cd..b4003e4 100644 --- a/docker/README.md +++ b/docker/README.md @@ -15,11 +15,7 @@ The running container can be configured through different environment variables: - `CONFIG`: Optional name of configuration file to use. - `HTTP_LISTEN`: Address of HTTP listener. -- `HTTP_READ_TIMEOUT`: HTTP socket read timeout in seconds. -- `HTTP_WRITE_TIMEOUT`: HTTP socket write timeout in seconds. - `HTTPS_LISTEN`: Address of HTTPS listener. -- `HTTPS_READ_TIMEOUT`: HTTPS socket read timeout in seconds. -- `HTTPS_WRITE_TIMEOUT`: HTTPS socket write timeout in seconds. - `HTTPS_CERTIFICATE`: Name of certificate file for the HTTPS listener. - `HTTPS_KEY`: Name of private key file for the HTTPS listener. - `HASH_KEY`: Secret value used to generate checksums of sessions (32 or 64 bytes). @@ -28,15 +24,11 @@ The running container can be configured through different environment variables: - `BACKENDS_ALLOWALL`: Allow all backends. Extremly insecure - use only for development! - `BACKENDS_ALLOWALL_SECRET`: Secret when `BACKENDS_ALLOWALL` is enabled. - `BACKENDS`: Space-separated list of backend ids. -- `BACKENDS_TIMEOUT`: Timeout in seconds for requests to backends. -- `CONNECTIONS_PER_HOST`: Maximum number of concurrent backend connections per host. -- `BACKEND__URLS`: Comma-separated list of urls of backend `ID` (where `ID` is the uppercase backend id). -- `BACKEND__URL`: Url of backend `ID` (where `ID` is the uppercase backend id, deprecated). +- `BACKEND__URL`: Url of backend `ID` (where `ID` is the uppercase backend id). - `BACKEND__SHARED_SECRET`: Shared secret for backend `ID` (where `ID` is the uppercase backend id). - `BACKEND__SESSION_LIMIT`: Optional session limit for backend `ID` (where `ID` is the uppercase backend id). - `BACKEND__MAX_STREAM_BITRATE`: Optional maximum bitrate for audio/video streams in backend `ID` (where `ID` is the uppercase backend id). - `BACKEND__MAX_SCREEN_BITRATE`: Optional maximum bitrate for screensharing streams in backend `ID` (where `ID` is the uppercase backend id). -- `FEDERATION_TIMEOUT`: Timeout for requests to federation targets in seconds. - `NATS_URL`: Optional URL of NATS server. - `ETCD_ENDPOINTS`: Static list of etcd endpoints (if etcd should be used). - `ETCD_DISCOVERY_SRV`: Alternative domain to use for DNS SRV configuration of etcd endpoints (if etcd should be used). @@ -49,15 +41,12 @@ The running container can be configured through different environment variables: - `USE_PROXY`: Set to `1` if proxy servers should be used as WebRTC backends. - `PROXY_TOKEN_ID`: Id of the token to use when connecting to proxy servers. - `PROXY_TOKEN_KEY`: Private key for the configured token id. -- `PROXY_TIMEOUT`: Timeout in seconds for requests to the proxy server. - `PROXY_URLS`: Space-separated list of proxy URLs to connect to. - `PROXY_DNS_DISCOVERY`: Enable DNS discovery on hostnames of configured static URLs. - `PROXY_ETCD`: Set to `1` if etcd should be used to configure proxy connections. - `PROXY_KEY_PREFIX`: Key prefix of proxy entries. - `MAX_STREAM_BITRATE`: Optional global maximum bitrate for audio/video streams. - `MAX_SCREEN_BITRATE`: Optional global maximum bitrate for screensharing streams. -- `ALLOWED_CANDIDATES`: List of IP addresses / subnets that are allowed to be used by clients in candidates. The allowed list has preference over the blocked list below. -- `BLOCKED_CANDIDATES`: List of IP addresses / subnets to filter from candidates received by clients. - `TURN_API_KEY`: API key that Janus will need to send when requesting TURN credentials. - `TURN_SECRET`: The shared secret to use for generating TURN credentials. - `TURN_SERVERS`: A comma-separated list of TURN servers to use. @@ -120,8 +109,6 @@ The running container can be configured through different environment variables: - `JANUS_URL`: Url to Janus server. - `MAX_STREAM_BITRATE`: Optional maximum bitrate for audio/video streams. - `MAX_SCREEN_BITRATE`: Optional maximum bitrate for screensharing streams. -- `ALLOWED_CANDIDATES`: List of IP addresses / subnets that are allowed to be used by clients in candidates. The allowed list has preference over the blocked list below. -- `BLOCKED_CANDIDATES`: List of IP addresses / subnets to filter from candidates received by clients. - `STATS_IPS`: Comma-separated list of IP addresses that are allowed to access the stats endpoint. - `TRUSTED_PROXIES`: Comma-separated list of IPs / networks that are trusted proxies. - `ETCD_ENDPOINTS`: Static list of etcd endpoints (if etcd should be used). diff --git a/docker/proxy/Dockerfile b/docker/proxy/Dockerfile index 4ecc624..fb78e03 100644 --- a/docker/proxy/Dockerfile +++ b/docker/proxy/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM} golang:1.26-alpine AS builder +FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder ARG TARGETARCH ARG TARGETOS @@ -12,8 +12,7 @@ RUN touch /.dockerenv && \ FROM alpine:3 ENV CONFIG=/config/proxy.conf -RUN addgroup -g 850 spreedbackend && \ - adduser -D --uid 850 -S -H -G spreedbackend spreedbackend && \ +RUN adduser -D spreedbackend && \ apk add --no-cache bash tzdata ca-certificates su-exec COPY --from=builder /workdir/bin/proxy /usr/bin/nextcloud-spreed-signaling-proxy diff --git a/docker/proxy/entrypoint.sh b/docker/proxy/entrypoint.sh index 116a9dd..5a2d2ff 100755 --- a/docker/proxy/entrypoint.sh +++ b/docker/proxy/entrypoint.sh @@ -96,12 +96,6 @@ if [ ! -f "$CONFIG" ]; then if [ -n "$MAX_SCREEN_BITRATE" ]; then sed -i "s|#maxscreenbitrate =.*|maxscreenbitrate = $MAX_SCREEN_BITRATE|" "$CONFIG" fi - if [ -n "$ALLOWED_CANDIDATES" ]; then - sed -i "s|#allowedcandidates =.*|allowedcandidates = $ALLOWED_CANDIDATES|" "$CONFIG" - fi - if [ -n "$BLOCKED_CANDIDATES" ]; then - sed -i "s|#blockedcandidates =.*|blockedcandidates = $BLOCKED_CANDIDATES|" "$CONFIG" - fi if [ -n "$TOKENS_ETCD" ]; then if [ -z "$HAS_ETCD" ]; then diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile index 643403b..9c59e7c 100644 --- a/docker/server/Dockerfile +++ b/docker/server/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM} golang:1.26-alpine AS builder +FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder ARG TARGETARCH ARG TARGETOS @@ -12,8 +12,7 @@ RUN touch /.dockerenv && \ FROM alpine:3 ENV CONFIG=/config/server.conf -RUN addgroup -g 850 spreedbackend && \ - adduser -D --uid 850 -S -H -G spreedbackend spreedbackend && \ +RUN adduser -D spreedbackend && \ apk add --no-cache bash tzdata ca-certificates su-exec COPY --from=builder /workdir/bin/signaling /usr/bin/nextcloud-spreed-signaling diff --git a/docker/server/entrypoint.sh b/docker/server/entrypoint.sh index 03d9996..dc3d08d 100755 --- a/docker/server/entrypoint.sh +++ b/docker/server/entrypoint.sh @@ -39,21 +39,8 @@ if [ ! -f "$CONFIG" ]; then if [ -n "$HTTP_LISTEN" ]; then sed -i "s|#listen = 127.0.0.1:8080|listen = $HTTP_LISTEN|" "$CONFIG" fi - if [ -n "$HTTP_READ_TIMEOUT" ]; then - sed -i "/HTTP socket/,/HTTP socket/ s|#readtimeout =.*|readtimeout = $HTTP_READ_TIMEOUT|" "$CONFIG" - fi - if [ -n "$HTTP_WRITE_TIMEOUT" ]; then - sed -i "/HTTP socket/,/HTTP socket/ s|#writetimeout =.*|writetimeout = $HTTP_WRITE_TIMEOUT|" "$CONFIG" - fi - if [ -n "$HTTPS_LISTEN" ]; then sed -i "s|#listen = 127.0.0.1:8443|listen = $HTTPS_LISTEN|" "$CONFIG" - if [ -n "$HTTPS_READ_TIMEOUT" ]; then - sed -i "/HTTPS socket/,/HTTPS socket/ s|#readtimeout =.*|readtimeout = $HTTPS_READ_TIMEOUT|" "$CONFIG" - fi - if [ -n "$HTTPS_WRITE_TIMEOUT" ]; then - sed -i "/HTTPS socket/,/HTTPS socket/ s|#writetimeout =.*|writetimeout = $HTTPS_WRITE_TIMEOUT|" "$CONFIG" - fi if [ -n "$HTTPS_CERTIFICATE" ]; then sed -i "s|certificate = /etc/nginx/ssl/server.crt|certificate = $HTTPS_CERTIFICATE|" "$CONFIG" @@ -77,9 +64,6 @@ if [ ! -f "$CONFIG" ]; then else sed -i "s|#url = nats://localhost:4222|url = nats://loopback|" "$CONFIG" fi - if [ -n "$FEDERATION_TIMEOUT" ]; then - sed -i "/federation/,/federation/ s|#timeout =.*|timeout = $FEDERATION_TIMEOUT|" "$CONFIG" - fi HAS_ETCD= if [ -n "$ETCD_ENDPOINTS" ]; then @@ -119,9 +103,6 @@ if [ ! -f "$CONFIG" ]; then if [ -n "$PROXY_TOKEN_KEY" ]; then sed -i "s|#token_key =.*|token_key = $PROXY_TOKEN_KEY|" "$CONFIG" fi - if [ -n "$PROXY_TIMEOUT" ]; then - sed -i "s|#proxytimeout =.*|proxytimeout = $PROXY_TIMEOUT|" "$CONFIG" - fi if [ -n "$PROXY_ETCD" ]; then if [ -z "$HAS_ETCD" ]; then @@ -150,12 +131,6 @@ if [ ! -f "$CONFIG" ]; then if [ -n "$MAX_SCREEN_BITRATE" ]; then sed -i "s|#maxscreenbitrate =.*|maxscreenbitrate = $MAX_SCREEN_BITRATE|" "$CONFIG" fi - if [ -n "$ALLOWED_CANDIDATES" ]; then - sed -i "s|#allowedcandidates =.*|allowedcandidates = $ALLOWED_CANDIDATES|" "$CONFIG" - fi - if [ -n "$BLOCKED_CANDIDATES" ]; then - sed -i "s|#blockedcandidates =.*|blockedcandidates = $BLOCKED_CANDIDATES|" "$CONFIG" - fi if [ -n "$SKIP_VERIFY" ]; then sed -i "s|#skipverify =.*|skipverify = $SKIP_VERIFY|" "$CONFIG" @@ -257,14 +232,6 @@ if [ ! -f "$CONFIG" ]; then sed -i "s|#secret = the-shared-secret-for-allowall|secret = $BACKENDS_ALLOWALL_SECRET|" "$CONFIG" fi - if [ -n "$BACKENDS_TIMEOUT" ]; then - sed -i "/requests to the backend/,/requests to the backend/ s|^timeout =.*|timeout = $BACKENDS_TIMEOUT|" "$CONFIG" - fi - - if [ -n "$CONNECTIONS_PER_HOST" ]; then - sed -i "s|connectionsperhost =.*|connectionsperhost = $CONNECTIONS_PER_HOST|" "$CONFIG" - fi - if [ -n "$BACKENDS" ]; then BACKENDS_CONFIG=${BACKENDS// /,} sed -i "s|#backends = .*|backends = $BACKENDS_CONFIG|" "$CONFIG" @@ -273,15 +240,9 @@ if [ ! -f "$CONFIG" ]; then for backend in $BACKENDS; do echo "[$backend]" >> "$CONFIG" - declare var="BACKEND_${backend^^}_URLS" + declare var="BACKEND_${backend^^}_URL" if [ -n "${!var}" ]; then - echo "urls = ${!var}" >> "$CONFIG" - else - declare var_compat="BACKEND_${backend^^}_URL" - if [ -n "${!var_compat}" ]; then - echo "Variable $var_compat is deprecated, use $var instead." - echo "urls = ${!var_compat}" >> "$CONFIG" - fi + echo "url = ${!var}" >> "$CONFIG" fi declare var="BACKEND_${backend^^}_SHARED_SECRET" @@ -305,8 +266,6 @@ if [ ! -f "$CONFIG" ]; then fi echo >> "$CONFIG" done - elif [ -n "$BACKENDS_COMPAT_ALLOWED" ]; then - sed -i "s|#backends =.*|allowed = $BACKENDS_COMPAT_ALLOWED\nsecret = $BACKENDS_COMPAT_SECRET|" "$CONFIG" fi fi diff --git a/docs/prometheus-metrics.md b/docs/prometheus-metrics.md index c75aa86..70d6ef9 100644 --- a/docs/prometheus-metrics.md +++ b/docs/prometheus-metrics.md @@ -52,28 +52,3 @@ The following metrics are available: | `signaling_http_client_pool_connections` | Gauge | 1.2.4 | The current number of HTTP client connections per host | `host` | | `signaling_throttle_delayed_total` | Counter | 1.2.5 | The total number of delayed requests | `action`, `delay` | | `signaling_throttle_bruteforce_total` | Counter | 1.2.5 | The total number of rejected bruteforce requests | `action` | -| `signaling_backend_client_requests_total` | Counter | 2.0.3 | The total number of backend client requests | `backend` | -| `signaling_backend_client_requests_duration` | Histogram | 2.0.3 | The duration of backend client requests in seconds | `backend` | -| `signaling_backend_client_requests_errors_total` | Counter | 2.0.3 | The total number of backend client requests that had an error | `backend`, `error` | -| `signaling_mcu_bandwidth` | Gauge | 2.1.0 | The current bandwidth in bytes per second | `direction` | -| `signaling_mcu_backend_usage` | Gauge | 2.1.0 | The current usage of signaling proxy backends in percent | `url`, `direction` | -| `signaling_mcu_backend_bandwidth` | Gauge | 2.1.0 | The current bandwidth of signaling proxy backends in bytes per second | `url`, `direction` | -| `signaling_proxy_load` | Gauge | 2.1.0 | The current load of the signaling proxy | | -| `signaling_client_rtt` | Histogram | 2.1.0 | The roundtrip time of WebSocket ping messages in milliseconds | | -| `signaling_mcu_selected_candidate_total` | Counter | 2.1.0 | Total number of selected candidates | `origin`, `type`, `transport`, `family` | -| `signaling_mcu_peerconnection_state_total` | Counter | 2.1.0 | Total number PeerConnection states | `state`, `reason` | -| `signaling_mcu_ice_state_total` | Counter | 2.1.0 | Total number of ICE connection states | `state` | -| `signaling_mcu_dtls_state_total` | Counter | 2.1.0 | Total number of DTLS connection states | `state` | -| `signaling_mcu_slow_link_total` | Counter | 2.1.0 | Total number of slow link events | `media`, `direction` | -| `signaling_mcu_media_rtt` | Histogram | 2.1.0 | The roundtrip time of WebRTC media in milliseconds | `media` | -| `signaling_mcu_media_jitter` | Histogram | 2.1.0 | The jitter of WebRTC media in milliseconds | `media`, `origin` | -| `signaling_mcu_media_codecs_total` | Counter | 2.1.0 | The total number of codecs | `media`, `codec` | -| `signaling_mcu_media_nacks_total` | Counter | 2.1.0 | The total number of NACKs | `media`, `direction` | -| `signaling_mcu_media_retransmissions_total` | Counter | 2.1.0 | The total number of received retransmissions | `media` | -| `signaling_mcu_media_bytes_total` | Counter | 2.1.0 | The total number of media bytes sent / received | `media`, `direction` | -| `signaling_mcu_media_lost_total` | Counter | 2.1.0 | The total number of lost media packets | `media`, `origin` | -| `signaling_client_bytes_total` | Counter | 2.1.0 | The total number of bytes sent to or received by clients | `direction` | -| `signaling_client_messages_total` | Counter | 2.1.0 | The total number of messages sent to or received by clients | `direction` | -| `signaling_call_sessions` | Gauge | 2.1.0 | The current number of sessions in a call | `backend`, `room`, `clienttype` | -| `signaling_call_sessions_total` | Counter | 2.1.0 | The total number of sessions in a call | `backend`, `clienttype` | -| `signaling_call_rooms_total` | Counter | 2.1.0 | The total number of rooms with an active call | `backend` | diff --git a/docs/requirements.txt b/docs/requirements.txt index 1794104..97531c7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ -jinja2==3.1.6 -markdown==3.10.2 +jinja2==3.1.5 +markdown==3.7 mkdocs==1.6.1 readthedocs-sphinx-search==0.3.2 -sphinx==9.1.0 -sphinx_rtd_theme==3.1.0 +sphinx==8.1.3 +sphinx_rtd_theme==3.0.2 diff --git a/docs/standalone-signaling-api-v1.md b/docs/standalone-signaling-api-v1.md index e6c5d17..7378914 100644 --- a/docs/standalone-signaling-api-v1.md +++ b/docs/standalone-signaling-api-v1.md @@ -173,15 +173,6 @@ future version. Clients should use the data from the [`welcome` message](#welcome-message) instead. -## Client features - -The following client feature flags are currently supported: -- `chat-relay`: The client understands chat message events containing the actual - message payload. -- `internal-incall`: The (internal) client can set the `inCall` flag. -- `start-dialout`: The (internal) client can start outgoing phone calls. - - ### Protocol version "1.0" For protocol version `1.0` in the `hello` request, the `params` from the `auth` @@ -294,18 +285,13 @@ authorized, the backend returns an error and the hello request will be rejected. ### Error codes -- `invalid_request`: The backend request for the v1 hello could not be authenticated. Check your shared secret. -- `invalid_hello_version`: The requested `hello` version is not supported. -- `auth_failed`: The session could not be authenticated. +- `unsupported-version`: The requested version is not supported. +- `auth-failed`: The session could not be authenticated. +- `too-many-sessions`: Too many sessions exist for this user id. - `invalid_backend`: The requested backend URL is not supported. - `invalid_client_type`: The [client type](#client-types) is not supported. -- `invalid_ticket`: The passed ticket in the v1 hello is invalid. -- `no_such_user`: The user id in the v1 hello does not exists. - `invalid_token`: The passed token is invalid (can happen for - [client type `internal`](#client-type-internal) or v2 requests). -- `token_not_valid_yet`: The token could be authenticated but is not valid yet. -- `token_expired`: The token could be authenticated but is expired. -- `too_many_requests`: Too many failed requests from this client. + [client type `internal`](#client-type-internal)). ### Client types @@ -403,7 +389,6 @@ server will return an error and a normal `hello` handshake has to be performed. ### Error codes - `no_such_session`: The session id is no longer valid. -- `too_many_requests`: Too many failed requests from this client. ## Releasing sessions @@ -466,10 +451,6 @@ Message format (Server -> Client): "roomid": "the-room-id", "properties": { ...additional room properties... - }, - "bandwidth": { - "maxstreambitrate": 1048576, - "maxscreenbitrate": 2097152 } } } @@ -478,10 +459,6 @@ Message format (Server -> Client): - The `roomid` will be empty if the client is no longer in a room. - Can be sent without a request if the server moves a client to a room / out of the current room or the properties of a room change. -- The optional `bandwidth` field contains information on the expected bandwidth - limits (in bits per second) for publishing in this room. If publishing is done - through signaling proxies or there already are other publishers in the room, - the actual limits might be lower than what is returned here. Message format (Server -> Client if already joined before): @@ -493,11 +470,9 @@ Message format (Server -> Client if already joined before): "code": "already_joined", "message": "Human readable error message", "details": { - "room": { - "roomid": "the-room-id", - "properties": { - ...additional room properties... - } + "roomid": "the-room-id", + "properties": { + ...additional room properties... } } } @@ -546,11 +521,8 @@ user, the backend returns an error and the room request will be rejected. ### Error codes -- `invalid_request`: The backend request could not be authenticated. Check your shared secret. - `no_such_room`: The requested room does not exist or the user is not invited to the room. -- `duplicate_session`: The given session already joined the room. -- `room_join_failed`: The Talk backend returned an unexpected response while joining the room. ## Join federated room @@ -823,9 +795,10 @@ Message format (Server -> Client, incall change): ## Room messages The server can notify clients about events that happened in a room. Currently -such messages are only sent out when chat messages are posted. +such messages are only sent out when chat messages are posted to notify clients +they should load the new messages. -Message format (Server -> Client, new messages available, refresh chat): +Message format (Server -> Client, chat messages available): { "type": "event" @@ -845,28 +818,6 @@ Message format (Server -> Client, new messages available, refresh chat): } -Message format (Server -> Client, new message was posted): - - { - "type": "event" - "event": { - "target": "room", - "type": "message", - "message": { - "roomid": "the-room-id", - "data": { - "type": "chat", - "chat": { - "comment": { - ...properties of the chat message... - } - } - } - } - } - } - - ## Sending messages between clients Messages between clients are sent realtime and not stored by the server, i.e. @@ -1274,23 +1225,6 @@ Transient data is supported if the server returns the `transient-data` feature id in the [hello response](#establish-connection). -### Transient session data - -Sessions can set data for a special key in the format `sd:` where -`` is the public id of the session updating the key. A session can -only set its own session data and the entry is removed automatically once the -session leaves the room (either explicitly or because it is expired). - -This can be used to publish local information on a given session to all other -sessions in a room (e.g. the display name of guest users) without having to -manually keep track of which other session that information would have to be -sent to and who already got it. - -Transient session data is supported if the server returns the -`transient-sessiondata` feature id in the -[hello response](#establish-connection). - - ### Set value Message format (Client -> Server): @@ -1361,9 +1295,7 @@ Message format (Server -> Client): ### Initial data When sessions initially join a room, they receive the current state of the -transient data. Please note that the initial data can be sent in multiple -events of type `initial` which must be combined to generate the total initial -data. +transient data. Message format (Server -> Client): @@ -1484,202 +1416,6 @@ The signaling server provides an internal API that can be called from Nextcloud to trigger events from the server side. -## Welcome message - -The welcome message at `/api/v1/welcome` can be retrieved using HTTP `GET` to -check if the signaling server is reachable and to get the version number and -supported features. - -A comma separated list of feature ids is in the `X-Spreed-Signaling-Features` -HTTP header, the version is available in the response body: - - { - "nextcloud-spreed-signaling": "Welcome", - "version": "1.2.3" - } - - -## Server info - -If the feature id `serverinfo` is supported, the server info API at -`/api/v1/serverinfo` can be called with HTTP `GET` to query information about -the server. - -Please note that the client calling this API must be allowed through the -`allowed_ips` option in the `[stats]` section. - - -### Example response with Janus backend - -Below is an example response of the serverinfo endpoint with a connected Janus -server: - - { - "version": "1.2.3", - "features": [ - "feature-1", - "feature-2", - ... - "serverinfo", - ... - ], - "sfu": { - "mode": "janus", - "janus": { - "url": "ws://localhost:8188/", - "connected": true, - "name": "Janus WebRTC Server", - "version": "1.3.1", - "author": "Meetecho s.r.l.", - "datachannels": true, - "fulltrickle": true, - "localip": "192.168.0.1", - "ipv6": false, - "videoroom": { - "name": "JANUS VideoRoom plugin", - "version": "0.0.10", - "author": "Meetecho s.r.l." - } - } - } - } - -If the backend is not connected, the value of `connected` in `janus` will be -`false` and most other entries will be missing. - - -### Example response with signaling proxy backends - -Below is an example response of the serverinfo endpoint with multiple signaling -proxies: - - { - "version": "1.2.3", - "features": [ - "feature-1", - "feature-2", - ... - "serverinfo", - ... - ], - "sfu": { - "mode": "proxy", - "proxies": [ - { - "url": "https://proxy.domain.tld/", - "ip": "192.168.0.1", - "connected": true, - "temporary": false, - "shutdown": false, - "uptime": "2025-03-05T18:09:35.435902408+01:00", - "version": "2.3.4", - "features": [ - "proxy-feature-1", - "proxy-feature-2", - ... - ], - "country": "DE", - "load": 0, - "bandwidth": { - "incoming": 0, - "outgoing": 0 - } - }, - { - "url": "https://proxy.domain.tld/", - "ip": "192.168.0.2", - "connected": false, - "temporary": false - } - ] - } - } - -The `ip` field will only be present if DNS discovery is enabled for resolving -proxy urls. -`uptime` is the ISO8601 time since the connection was established to the proxy. -`country` will only be returned if configured on the proxy. -`load` is an arbitrary value used to order signaling proxies when selecting the -proxy to use for publishing new streams. -`bandwidth` contains the percentage of incoming / outgoing bandwith utilization -for streams on the proxy. Only present if a bandwidth limit is configured on -the proxy. - - -### NATS connection - -Information about the NATS connection are also returned by the serverinfo -endpoint: - - { - "urls": [ - "nats://localhost:4222" - ], - "connected": true, - "serverurl": "nats://localhost:4222", - "serverid": "556c9de63ac214e53a9b976b2e5305d8", - "version": "0.6.8" - } - - -### GRPC connections - -In clustered mode, the signaling server has GRPC connections to other instances -which are included in the serverinfo response: - - [ - { - "target": "192.168.1.1:8080", - "connected": true, - "version": "1.2.3" - }, - { - "target": "192.168.1.2:8080", - "connected": true, - "version": "1.2.3" - } - ] - - -### ETCD cluster - -Some configuration can be loaded from an etcd cluster, the serverinfo response -also contains information on that connection: - - { - "endpoints": [ - "192.168.4.1:2379", - "192.168.4.2:2379" - ], - "active": "etcd-endpoints://0xc0001fba40/192.168.4.2:2379", - "connected": true - } - - -### Dialout session - -If a SIP bridge with support for dial-out is connected, the serverinfo response -will contain an additional property `dialout` with the following contents: - - [ - { - "sessionid": "the-session-id", - "connected": true, - "address": "192.168.1.0", - "useragent": "spreed-webrtc-sip-bridge/1.2.3", - "version": "1.2.3", - "features": [ - "start-dialout", - "datachannels", - "encryption" - ] - } - ] - -If the connection between SIP bridge and signaling server is interrupted, the -`connected` property will be `false` and details on the session (`address`, `useragent` and `features`) omitted. - - ## Rooms API The base URL for the rooms API is `/api/vi/room/`, all requests must be @@ -1837,66 +1573,6 @@ Message format (Backend -> Server) } -#### Send chat room message - -Usually, clients do a regular poll against Talk to fetch chat messages. In order -to reduce the load, the clients can be notified about new messages (which then -causes them to fetch them), or the messages can be sent directly to them. - -Depending on the client feature `chat-relay`, clients will either get the -event with `"refresh": true`, or they get the full chat `comment` object. This -is available if the signaling server supports the feature flag `chat-relay`. -If not, the full event will be sent to the clients (containing both `refresh` -and `comment`). - -Message format (Backend -> Server) - - { - "type": "message" - "message" { - "data": { - "type": "chat", - "chat": { - "refresh": true, - "comment": { - ...properties of the comment written in the chat... - } - } - } - } - } - - -The signaling server also supports combining multiple chat comments into one -request which will then be sent out individually to clients. - -Message format (Backend -> Server) - - { - "type": "message" - "message" { - "data": { - "type": "chat", - "chat": { - "refresh": true, - "comments": [ - { - ...properties of the first comment written in the chat... - }, - { - ...properties of the second comment written in the chat... - } - ] - } - } - } - } - -In this case, clients will either receive a single `"refresh": true` message -(if they don't support `chat-relay`) or multiple messages with the different -comments. - - ### Notify sessions to switch to a different room This can be used to let sessions in a room know that they switch to a different @@ -1968,7 +1644,7 @@ Message format (Backend -> Server) "dialout" { "number": "e164-target-number", "options": { - ...additional options... + ...arbitrary options that will be sent back to validate... } } } @@ -1976,26 +1652,6 @@ Message format (Backend -> Server) Please note that this requires a connected internal client that supports dialout (e.g. the SIP bridge). -The `options` will be sent to Nextcloud Talk for validation of the dialout -request. A field `caller` can be included containing the data that should be -sent as `From` header in the outgoing call, or a field `anonymous` with value -`true` to trigger an anonymous outgoing call (CLIR). - -Example request (dialout to `+49123456789` and use `+491122334455` as caller): - - { - "type": "dialout" - "dialout" { - "number": "+49123456789", - "options": { - "attendeeId": "abcdef", - "actorType": "actor-type", - "actorId": "the-actor", - "caller": "+491122334455" - } - } - } - Message format (Server -> Backend, request was accepted) { diff --git a/etcd/api.go b/etcd/api.go deleted file mode 100644 index b93c38f..0000000 --- a/etcd/api.go +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package etcd - -import ( - "context" - "errors" - "fmt" - "net/url" - "slices" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - clientv3 "go.etcd.io/etcd/client/v3" -) - -type ClientListener interface { - EtcdClientCreated(client Client) -} - -type ClientWatcher interface { - EtcdWatchCreated(client Client, key string) - EtcdKeyUpdated(client Client, key string, value []byte, prevValue []byte) - EtcdKeyDeleted(client Client, key string, prevValue []byte) -} - -type Client interface { - IsConfigured() bool - WaitForConnection(ctx context.Context) error - GetServerInfoEtcd() *BackendServerInfoEtcd - Close() error - - AddListener(listener ClientListener) - RemoveListener(listener ClientListener) - - Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) - Watch(ctx context.Context, key string, nextRevision int64, watcher ClientWatcher, opts ...clientv3.OpOption) (int64, error) -} - -// Information on a backend in the etcd cluster. - -type BackendInformationEtcd struct { - // Compat setting. - Url string `json:"url,omitempty"` - - Urls []string `json:"urls,omitempty"` - ParsedUrls []*url.URL `json:"-"` - Secret string `json:"secret"` - - MaxStreamBitrate api.Bandwidth `json:"maxstreambitrate,omitempty"` - MaxScreenBitrate api.Bandwidth `json:"maxscreenbitrate,omitempty"` - - SessionLimit uint64 `json:"sessionlimit,omitempty"` -} - -func (p *BackendInformationEtcd) CheckValid() (err error) { - if p.Secret == "" { - return errors.New("secret missing") - } - - if len(p.Urls) > 0 { - slices.Sort(p.Urls) - p.Urls = slices.Compact(p.Urls) - seen := make(map[string]bool) - outIdx := 0 - for _, u := range p.Urls { - parsedUrl, err := url.Parse(u) - if err != nil { - return fmt.Errorf("invalid url %s: %w", u, err) - } - - var changed bool - if parsedUrl, changed = internal.CanonicalizeUrl(parsedUrl); changed { - u = parsedUrl.String() - } - p.Urls[outIdx] = u - if seen[u] { - continue - } - seen[u] = true - p.ParsedUrls = append(p.ParsedUrls, parsedUrl) - outIdx++ - } - if len(p.Urls) != outIdx { - clear(p.Urls[outIdx:]) - p.Urls = p.Urls[:outIdx] - } - } else if p.Url != "" { - parsedUrl, err := url.Parse(p.Url) - if err != nil { - return fmt.Errorf("invalid url: %w", err) - } - var changed bool - if parsedUrl, changed = internal.CanonicalizeUrl(parsedUrl); changed { - p.Url = parsedUrl.String() - } - - p.Urls = append(p.Urls, p.Url) - p.ParsedUrls = append(p.ParsedUrls, parsedUrl) - } else { - return errors.New("urls missing") - } - - return nil -} - -type BackendServerInfoEtcd struct { - Endpoints []string `json:"endpoints"` - - Active string `json:"active,omitempty"` - Connected *bool `json:"connected,omitempty"` -} diff --git a/etcd/api_easyjson.go b/etcd/api_easyjson.go deleted file mode 100644 index 16f93ba..0000000 --- a/etcd/api_easyjson.go +++ /dev/null @@ -1,308 +0,0 @@ -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. - -package etcd - -import ( - json "encoding/json" - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" - api "github.com/strukturag/nextcloud-spreed-signaling/v2/api" -) - -// suppress unused package warning -var ( - _ *json.RawMessage - _ *jlexer.Lexer - _ *jwriter.Writer - _ easyjson.Marshaler -) - -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd(in *jlexer.Lexer, out *BackendServerInfoEtcd) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "endpoints": - if in.IsNull() { - in.Skip() - out.Endpoints = nil - } else { - in.Delim('[') - if out.Endpoints == nil { - if !in.IsDelim(']') { - out.Endpoints = make([]string, 0, 4) - } else { - out.Endpoints = []string{} - } - } else { - out.Endpoints = (out.Endpoints)[:0] - } - for !in.IsDelim(']') { - var v1 string - if in.IsNull() { - in.Skip() - } else { - v1 = string(in.String()) - } - out.Endpoints = append(out.Endpoints, v1) - in.WantComma() - } - in.Delim(']') - } - case "active": - if in.IsNull() { - in.Skip() - } else { - out.Active = string(in.String()) - } - case "connected": - if in.IsNull() { - in.Skip() - out.Connected = nil - } else { - if out.Connected == nil { - out.Connected = new(bool) - } - if in.IsNull() { - in.Skip() - } else { - *out.Connected = bool(in.Bool()) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd(out *jwriter.Writer, in BackendServerInfoEtcd) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"endpoints\":" - out.RawString(prefix[1:]) - if in.Endpoints == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v2, v3 := range in.Endpoints { - if v2 > 0 { - out.RawByte(',') - } - out.String(string(v3)) - } - out.RawByte(']') - } - } - if in.Active != "" { - const prefix string = ",\"active\":" - out.RawString(prefix) - out.String(string(in.Active)) - } - if in.Connected != nil { - const prefix string = ",\"connected\":" - out.RawString(prefix) - out.Bool(bool(*in.Connected)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoEtcd) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoEtcd) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoEtcd) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoEtcd) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd1(in *jlexer.Lexer, out *BackendInformationEtcd) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "url": - if in.IsNull() { - in.Skip() - } else { - out.Url = string(in.String()) - } - case "urls": - if in.IsNull() { - in.Skip() - out.Urls = nil - } else { - in.Delim('[') - if out.Urls == nil { - if !in.IsDelim(']') { - out.Urls = make([]string, 0, 4) - } else { - out.Urls = []string{} - } - } else { - out.Urls = (out.Urls)[:0] - } - for !in.IsDelim(']') { - var v4 string - if in.IsNull() { - in.Skip() - } else { - v4 = string(in.String()) - } - out.Urls = append(out.Urls, v4) - in.WantComma() - } - in.Delim(']') - } - case "secret": - if in.IsNull() { - in.Skip() - } else { - out.Secret = string(in.String()) - } - case "maxstreambitrate": - if in.IsNull() { - in.Skip() - } else { - out.MaxStreamBitrate = api.Bandwidth(in.Uint64()) - } - case "maxscreenbitrate": - if in.IsNull() { - in.Skip() - } else { - out.MaxScreenBitrate = api.Bandwidth(in.Uint64()) - } - case "sessionlimit": - if in.IsNull() { - in.Skip() - } else { - out.SessionLimit = uint64(in.Uint64()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd1(out *jwriter.Writer, in BackendInformationEtcd) { - out.RawByte('{') - first := true - _ = first - if in.Url != "" { - const prefix string = ",\"url\":" - first = false - out.RawString(prefix[1:]) - out.String(string(in.Url)) - } - if len(in.Urls) != 0 { - const prefix string = ",\"urls\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - { - out.RawByte('[') - for v5, v6 := range in.Urls { - if v5 > 0 { - out.RawByte(',') - } - out.String(string(v6)) - } - out.RawByte(']') - } - } - { - const prefix string = ",\"secret\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.Secret)) - } - if in.MaxStreamBitrate != 0 { - const prefix string = ",\"maxstreambitrate\":" - out.RawString(prefix) - out.Uint64(uint64(in.MaxStreamBitrate)) - } - if in.MaxScreenBitrate != 0 { - const prefix string = ",\"maxscreenbitrate\":" - out.RawString(prefix) - out.Uint64(uint64(in.MaxScreenBitrate)) - } - if in.SessionLimit != 0 { - const prefix string = ",\"sessionlimit\":" - out.RawString(prefix) - out.Uint64(uint64(in.SessionLimit)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendInformationEtcd) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd1(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendInformationEtcd) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd1(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendInformationEtcd) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd1(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendInformationEtcd) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Etcd1(l, v) -} diff --git a/etcd/api_test.go b/etcd/api_test.go deleted file mode 100644 index 887a4ee..0000000 --- a/etcd/api_test.go +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package etcd - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestValidateBackendInformationEtcd(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - testcases := []struct { - b BackendInformationEtcd - expectedError string - expectedUrls []string - }{ - { - b: BackendInformationEtcd{}, - expectedError: "secret missing", - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - }, - expectedError: "urls missing", - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Url: "https://foo\n", - }, - expectedError: "invalid url", - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Urls: []string{"https://foo\n"}, - }, - expectedError: "invalid url", - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Urls: []string{"https://foo", "https://foo\n"}, - }, - expectedError: "invalid url", - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Url: "https://foo:443", - }, - expectedUrls: []string{"https://foo"}, - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Urls: []string{"https://foo:443"}, - }, - expectedUrls: []string{"https://foo"}, - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Url: "https://foo:8443", - }, - expectedUrls: []string{"https://foo:8443"}, - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Urls: []string{"https://foo:8443"}, - }, - expectedUrls: []string{"https://foo:8443"}, - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Urls: []string{"https://foo", "https://bar", "https://foo"}, - }, - expectedUrls: []string{"https://bar", "https://foo"}, - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Urls: []string{"https://foo", "https://bar", "https://foo:443", "https://zaz"}, - }, - expectedUrls: []string{"https://bar", "https://foo", "https://zaz"}, - }, - { - b: BackendInformationEtcd{ - Secret: "verysecret", - Urls: []string{"https://foo:443", "https://bar", "https://foo", "https://zaz"}, - }, - expectedUrls: []string{"https://bar", "https://foo", "https://zaz"}, - }, - } - - for idx, tc := range testcases { - if tc.expectedError == "" { - if assert.NoError(tc.b.CheckValid(), "failed for testcase %d", idx) { - assert.Equal(tc.expectedUrls, tc.b.Urls, "failed for testcase %d", idx) - var urls []string - for _, u := range tc.b.ParsedUrls { - urls = append(urls, u.String()) - } - assert.Equal(tc.expectedUrls, urls, "failed for testcase %d", idx) - } - } else { - assert.ErrorContains(tc.b.CheckValid(), tc.expectedError, "failed for testcase %d, got %+v", idx, tc.b.ParsedUrls) - } - } -} diff --git a/etcd/test/etcd.go b/etcd/test/etcd.go deleted file mode 100644 index e29499b..0000000 --- a/etcd/test/etcd.go +++ /dev/null @@ -1,466 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "bytes" - "context" - "errors" - "net" - "net/url" - "os" - "slices" - "strconv" - "strings" - "sync" - "testing" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/api/v3/mvccpb" - clientv3 "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/server/v3/embed" - "go.etcd.io/etcd/server/v3/lease" - "go.uber.org/zap" - "go.uber.org/zap/zaptest" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" -) - -var ( - etcdListenUrl = "http://localhost:8080" -) - -type EtcdServer struct { - embed *embed.Etcd -} - -func (s *EtcdServer) URL() *url.URL { - return &s.embed.Config().ListenClientUrls[0] -} - -func (s *EtcdServer) SetValue(key string, value []byte) { - if kv := s.embed.Server.KV(); kv != nil { - kv.Put([]byte(key), value, lease.NoLease) - kv.Commit() - } -} - -func (s *EtcdServer) DeleteValue(key string) { - if kv := s.embed.Server.KV(); kv != nil { - kv.DeleteRange([]byte(key), nil) - kv.Commit() - } -} - -func NewServerForTest(t *testing.T) *EtcdServer { - t.Helper() - require := require.New(t) - cfg := embed.NewConfig() - cfg.Dir = t.TempDir() - os.Chmod(cfg.Dir, 0700) // nolint - cfg.LogLevel = "warn" - cfg.Name = "signalingtest" - cfg.ZapLoggerBuilder = embed.NewZapLoggerBuilder(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) - - u, err := url.Parse(etcdListenUrl) - require.NoError(err) - - // Find a free port to bind the server to. - var etcd *embed.Etcd - for port := 50000; port < 50100; port++ { - u.Host = net.JoinHostPort("localhost", strconv.Itoa(port)) - cfg.ListenClientUrls = []url.URL{*u} - cfg.AdvertiseClientUrls = []url.URL{*u} - httpListener := u - httpListener.Host = net.JoinHostPort("localhost", strconv.Itoa(port+1)) - cfg.ListenClientHttpUrls = []url.URL{*httpListener} - peerListener := u - peerListener.Host = net.JoinHostPort("localhost", strconv.Itoa(port+2)) - cfg.ListenPeerUrls = []url.URL{*peerListener} - cfg.AdvertisePeerUrls = []url.URL{*peerListener} - cfg.InitialCluster = "signalingtest=" + peerListener.String() - etcd, err = embed.StartEtcd(cfg) - if test.IsErrorAddressAlreadyInUse(err) { - continue - } - - require.NoError(err) - break - } - require.NotNil(etcd, "could not find free port") - - t.Cleanup(func() { - etcd.Close() - <-etcd.Server.StopNotify() - }) - // Wait for server to be ready. - <-etcd.Server.ReadyNotify() - - server := &EtcdServer{ - embed: etcd, - } - return server -} - -func NewEtcdClientForTest(t *testing.T, server *EtcdServer) etcd.Client { - t.Helper() - - logger := logtest.NewLoggerForTest(t) - - config := goconf.NewConfigFile() - config.AddOption("etcd", "endpoints", server.URL().String()) - - client, err := etcd.NewClient(logger, config, "") - require.NoError(t, err) - - t.Cleanup(func() { - assert.NoError(t, client.Close()) - }) - - return client -} - -type testWatch struct { - key string - op clientv3.Op - rev int64 - - watcher etcd.ClientWatcher -} - -type testClient struct { - mu sync.Mutex - server *Server - - // +checklocks:mu - closed bool - closeCh chan struct{} - processCh chan func() - // +checklocks:mu - listeners []etcd.ClientListener - // +checklocks:mu - watchers []*testWatch -} - -func newTestClient(server *Server) *testClient { - client := &testClient{ - server: server, - closeCh: make(chan struct{}), - processCh: make(chan func(), 1), - } - go func() { - defer close(client.closeCh) - for { - f := <-client.processCh - if f == nil { - return - } - - f() - } - }() - return client -} - -func (c *testClient) IsConfigured() bool { - return true -} - -func (c *testClient) WaitForConnection(ctx context.Context) error { - return nil -} - -func (c *testClient) GetServerInfoEtcd() *etcd.BackendServerInfoEtcd { - return &etcd.BackendServerInfoEtcd{} -} - -func (c *testClient) Close() error { - c.mu.Lock() - defer c.mu.Unlock() - - if c.closed { - return nil - } - - c.closed = true - c.server.removeClient(c) - close(c.processCh) - <-c.closeCh - return nil -} - -func (c *testClient) AddListener(listener etcd.ClientListener) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.closed { - return - } - - c.listeners = append(c.listeners, listener) - c.processCh <- func() { - listener.EtcdClientCreated(c) - } -} - -func (c *testClient) RemoveListener(listener etcd.ClientListener) { - c.mu.Lock() - defer c.mu.Unlock() - - c.listeners = slices.DeleteFunc(c.listeners, func(l etcd.ClientListener) bool { - return l == listener - }) -} - -func (c *testClient) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { - keys, values, revision := c.server.getValues(key, 0, opts...) - response := &clientv3.GetResponse{ - Count: int64(len(values)), - Header: &etcdserverpb.ResponseHeader{ - Revision: revision, - }, - } - for idx, key := range keys { - response.Kvs = append(response.Kvs, &mvccpb.KeyValue{ - Key: []byte(key), - Value: values[idx], - }) - } - return response, nil -} - -func (c *testClient) notifyUpdated(key string, oldValue []byte, newValue []byte) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.closed { - return - } - - for _, w := range c.watchers { - if withPrefix := w.op.IsOptsWithPrefix(); (withPrefix && strings.HasPrefix(key, w.key)) || (!withPrefix && key == w.key) { - c.processCh <- func() { - w.watcher.EtcdKeyUpdated(c, key, newValue, oldValue) - } - } - } -} - -func (c *testClient) notifyDeleted(key string, oldValue []byte) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.closed { - return - } - - for _, w := range c.watchers { - if withPrefix := w.op.IsOptsWithPrefix(); (withPrefix && strings.HasPrefix(key, w.key)) || (!withPrefix && key == w.key) { - c.processCh <- func() { - w.watcher.EtcdKeyDeleted(c, key, oldValue) - } - } - } -} - -func (c *testClient) addWatcher(w *testWatch, opts ...clientv3.OpOption) error { - keys, values, _ := c.server.getValues(w.key, w.rev, opts...) - - c.mu.Lock() - defer c.mu.Unlock() - - if c.closed { - return errors.New("closed") - } - - c.watchers = append(c.watchers, w) - c.processCh <- func() { - w.watcher.EtcdWatchCreated(c, w.key) - } - - for idx, key := range keys { - c.processCh <- func() { - w.watcher.EtcdKeyUpdated(c, key, values[idx], nil) - } - } - - return nil -} - -func (c *testClient) Watch(ctx context.Context, key string, nextRevision int64, watcher etcd.ClientWatcher, opts ...clientv3.OpOption) (int64, error) { - w := &testWatch{ - key: key, - rev: nextRevision, - - watcher: watcher, - } - for _, o := range opts { - o(&w.op) - } - - if err := c.addWatcher(w, opts...); err != nil { - return 0, err - } - - select { - case <-c.closeCh: - // Client is closed. - case <-ctx.Done(): - // Watch context was cancelled / timed out. - } - return c.server.getRevision(), nil -} - -type testServerValue struct { - value []byte - revision int64 -} - -type Server struct { - t *testing.T - mu sync.Mutex - // +checklocks:mu - clients []*testClient - // +checklocks:mu - values map[string]*testServerValue - // +checklocks:mu - revision int64 -} - -func (s *Server) newClient() *testClient { - client := newTestClient(s) - s.addClient(client) - return client -} - -func (s *Server) addClient(client *testClient) { - s.mu.Lock() - defer s.mu.Unlock() - - s.clients = append(s.clients, client) -} - -func (s *Server) removeClient(client *testClient) { - s.mu.Lock() - defer s.mu.Unlock() - - s.clients = slices.DeleteFunc(s.clients, func(c *testClient) bool { - return c == client - }) -} - -func (s *Server) getRevision() int64 { - s.mu.Lock() - defer s.mu.Unlock() - - return s.revision -} - -func (s *Server) getValues(key string, minRevision int64, opts ...clientv3.OpOption) (keys []string, values [][]byte, revision int64) { - s.mu.Lock() - defer s.mu.Unlock() - - var op clientv3.Op - for _, o := range opts { - o(&op) - } - if op.IsOptsWithPrefix() { - for k, value := range s.values { - if minRevision > 0 && value.revision < minRevision { - continue - } - if strings.HasPrefix(k, key) { - keys = append(keys, k) - values = append(values, value.value) - } - } - } else { - if value, found := s.values[key]; found && (minRevision == 0 || value.revision >= minRevision) { - keys = append(keys, key) - values = append(values, value.value) - } - } - - revision = s.revision - return -} - -func (s *Server) SetValue(key string, value []byte) { - s.mu.Lock() - defer s.mu.Unlock() - - prev, found := s.values[key] - if found && bytes.Equal(prev.value, value) { - return - } - - if s.values == nil { - s.values = make(map[string]*testServerValue) - } - if prev == nil { - prev = &testServerValue{} - s.values[key] = prev - } - s.revision++ - prevValue := prev.value - prev.value = value - prev.revision = s.revision - - for _, c := range s.clients { - c.notifyUpdated(key, prevValue, value) - } -} - -func (s *Server) DeleteValue(key string) { - s.mu.Lock() - defer s.mu.Unlock() - - prev, found := s.values[key] - if !found { - return - } - - delete(s.values, key) - s.revision++ - - for _, c := range s.clients { - c.notifyDeleted(key, prev.value) - } -} - -func NewClientForTest(t *testing.T) (*Server, etcd.Client) { - t.Helper() - server := &Server{ - t: t, - revision: 1, - } - client := server.newClient() - t.Cleanup(func() { - assert.NoError(t, client.Close()) - }) - return server, client -} diff --git a/etcd/test/etcd_test.go b/etcd/test/etcd_test.go deleted file mode 100644 index 652112e..0000000 --- a/etcd/test/etcd_test.go +++ /dev/null @@ -1,319 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - clientv3 "go.etcd.io/etcd/client/v3" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" -) - -var ( - testTimeout = 10 * time.Second -) - -type updateEvent struct { - key string - value string - prev []byte -} - -type deleteEvent struct { - key string - prev []byte -} - -type testWatcher struct { - created chan struct{} - updated chan updateEvent - deleted chan deleteEvent -} - -func newTestWatcher() *testWatcher { - return &testWatcher{ - created: make(chan struct{}), - updated: make(chan updateEvent), - deleted: make(chan deleteEvent), - } -} - -func (w *testWatcher) EtcdWatchCreated(client etcd.Client, key string) { - close(w.created) -} - -func (w *testWatcher) EtcdKeyUpdated(client etcd.Client, key string, value []byte, prevValue []byte) { - w.updated <- updateEvent{ - key: key, - value: string(value), - prev: prevValue, - } -} - -func (w *testWatcher) EtcdKeyDeleted(client etcd.Client, key string, prevValue []byte) { - w.deleted <- deleteEvent{ - key: key, - prev: prevValue, - } -} - -type serverInterface interface { - SetValue(key string, value []byte) - DeleteValue(key string) -} - -type testClientListener struct { - called chan struct{} -} - -func (l *testClientListener) EtcdClientCreated(c etcd.Client) { - close(l.called) -} - -func testServerWatch(t *testing.T, server serverInterface, client etcd.Client) { - require := require.New(t) - assert := assert.New(t) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - cancelCtx, cancel := context.WithCancel(ctx) - defer cancel() - - assert.True(client.IsConfigured(), "should be configured") - require.NoError(client.WaitForConnection(ctx)) - - listener := &testClientListener{ - called: make(chan struct{}), - } - client.AddListener(listener) - defer client.RemoveListener(listener) - - select { - case <-listener.called: - case <-ctx.Done(): - require.NoError(ctx.Err()) - } - - watcher := newTestWatcher() - - go func() { - if _, err := client.Watch(cancelCtx, "foo", 0, watcher); err != nil { - assert.ErrorIs(err, context.Canceled) - } - }() - - select { - case <-watcher.created: - case <-ctx.Done(): - require.NoError(ctx.Err()) - } - - key := "foo" - value := "bar" - server.SetValue("foo", []byte(value)) - select { - case evt := <-watcher.updated: - assert.Equal(key, evt.key) - assert.Equal(value, evt.value) - assert.Empty(evt.prev) - case <-ctx.Done(): - require.NoError(ctx.Err()) - } - - if response, err := client.Get(ctx, "foo"); assert.NoError(err) { - assert.EqualValues(1, response.Count) - if assert.Len(response.Kvs, 1) { - assert.Equal(key, string(response.Kvs[0].Key)) - assert.Equal(value, string(response.Kvs[0].Value)) - } - } - if response, err := client.Get(ctx, "f"); assert.NoError(err) { - assert.EqualValues(0, response.Count) - assert.Empty(response.Kvs) - } - if response, err := client.Get(ctx, "f", clientv3.WithPrefix()); assert.NoError(err) { - assert.EqualValues(1, response.Count) - if assert.Len(response.Kvs, 1) { - assert.Equal(key, string(response.Kvs[0].Key)) - assert.Equal(value, string(response.Kvs[0].Value)) - } - } - - server.DeleteValue("foo") - select { - case evt := <-watcher.deleted: - assert.Equal(key, evt.key) - assert.Equal(value, string(evt.prev)) - case <-ctx.Done(): - require.NoError(ctx.Err()) - } - - select { - case evt := <-watcher.updated: - assert.Fail("unexpected update event", "got %+v", evt) - case evt := <-watcher.deleted: - assert.Fail("unexpected deleted event", "got %+v", evt) - default: - } -} - -func TestServerWatch_Mock(t *testing.T) { - t.Parallel() - - server, client := NewClientForTest(t) - testServerWatch(t, server, client) -} - -func TestServerWatch_Real(t *testing.T) { - t.Parallel() - - server := NewServerForTest(t) - client := NewEtcdClientForTest(t, server) - testServerWatch(t, server, client) -} - -func testServerWatchInitialData(t *testing.T, server serverInterface, client etcd.Client) { - require := require.New(t) - assert := assert.New(t) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - key := "foo" - value := "bar" - server.SetValue("foo", []byte(value)) - - cancelCtx, cancel := context.WithCancel(ctx) - defer cancel() - - watcher := newTestWatcher() - - go func() { - if _, err := client.Watch(cancelCtx, "foo", 1, watcher); err != nil { - assert.ErrorIs(err, context.Canceled) - } - }() - - select { - case <-watcher.created: - case <-ctx.Done(): - require.NoError(ctx.Err()) - } - - select { - case evt := <-watcher.updated: - assert.Equal(key, evt.key) - assert.Equal(value, evt.value) - assert.Empty(evt.prev) - case <-ctx.Done(): - require.NoError(ctx.Err()) - } - - select { - case evt := <-watcher.updated: - assert.Fail("unexpected update event", "got %+v", evt) - case evt := <-watcher.deleted: - assert.Fail("unexpected deleted event", "got %+v", evt) - default: - } -} - -func TestServerWatchInitialData_Mock(t *testing.T) { - t.Parallel() - - server, client := NewClientForTest(t) - testServerWatchInitialData(t, server, client) -} - -func TestServerWatchInitialData_Real(t *testing.T) { - t.Parallel() - - server := NewServerForTest(t) - client := NewEtcdClientForTest(t, server) - testServerWatchInitialData(t, server, client) -} - -func testServerWatchInitialOldData(t *testing.T, server serverInterface, client etcd.Client) { - require := require.New(t) - assert := assert.New(t) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - key := "foo" - value := "bar" - server.SetValue("foo", []byte(value)) - - response, err := client.Get(ctx, key) - require.NoError(err) - - if assert.EqualValues(1, response.Count) && assert.Len(response.Kvs, 1) { - assert.Equal(key, string(response.Kvs[0].Key)) - assert.Equal(value, string(response.Kvs[0].Value)) - } - - cancelCtx, cancel := context.WithCancel(ctx) - defer cancel() - - watcher := newTestWatcher() - - go func() { - if _, err := client.Watch(cancelCtx, "foo", response.Header.GetRevision()+1, watcher); err != nil { - assert.ErrorIs(err, context.Canceled) - } - }() - - select { - case <-watcher.created: - case <-ctx.Done(): - require.NoError(ctx.Err()) - } - - select { - case evt := <-watcher.updated: - assert.Fail("unexpected update event", "got %+v", evt) - case evt := <-watcher.deleted: - assert.Fail("unexpected deleted event", "got %+v", evt) - default: - } -} - -func TestServerWatchInitialOldData_Mock(t *testing.T) { - t.Parallel() - - server, client := NewClientForTest(t) - testServerWatchInitialOldData(t, server, client) -} - -func TestServerWatchInitialOldData_Real(t *testing.T) { - t.Parallel() - - server := NewServerForTest(t) - client := NewEtcdClientForTest(t, server) - testServerWatchInitialOldData(t, server, client) -} diff --git a/etcd/client.go b/etcd_client.go similarity index 62% rename from etcd/client.go rename to etcd_client.go index 1c60687..ea1b64d 100644 --- a/etcd/client.go +++ b/etcd_client.go @@ -19,13 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package etcd +package signaling import ( "context" "errors" "fmt" - "slices" + "log" + "strings" "sync" "sync/atomic" "time" @@ -36,31 +37,28 @@ import ( clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "google.golang.org/grpc/connectivity" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) -var ( - initialWaitDelay = time.Second - maxWaitDelay = 8 * time.Second -) - -type etcdClient struct { - logger log.Logger - compatSection string - - mu sync.Mutex - client atomic.Value - // +checklocks:mu - listeners map[ClientListener]bool +type EtcdClientListener interface { + EtcdClientCreated(client *EtcdClient) } -func NewClient(logger log.Logger, config *goconf.ConfigFile, compatSection string) (Client, error) { - result := &etcdClient{ - logger: logger, +type EtcdClientWatcher interface { + EtcdWatchCreated(client *EtcdClient, key string) + EtcdKeyUpdated(client *EtcdClient, key string, value []byte, prevValue []byte) + EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte) +} + +type EtcdClient struct { + compatSection string + + mu sync.Mutex + client atomic.Value + listeners map[EtcdClientListener]bool +} + +func NewEtcdClient(config *goconf.ConfigFile, compatSection string) (*EtcdClient, error) { + result := &EtcdClient{ compatSection: compatSection, } if err := result.load(config, false); err != nil { @@ -70,47 +68,33 @@ func NewClient(logger log.Logger, config *goconf.ConfigFile, compatSection strin return result, nil } -func (c *etcdClient) GetServerInfoEtcd() *BackendServerInfoEtcd { - client := c.getEtcdClient() - if client == nil { - return nil - } - - result := &BackendServerInfoEtcd{ - Endpoints: client.Endpoints(), - } - - conn := client.ActiveConnection() - if conn != nil { - result.Active = conn.Target() - result.Connected = internal.MakePtr(conn.GetState() == connectivity.Ready) - } - - return result -} - -func (c *etcdClient) getConfigStringWithFallback(config *goconf.ConfigFile, option string) string { +func (c *EtcdClient) getConfigStringWithFallback(config *goconf.ConfigFile, option string) string { value, _ := config.GetString("etcd", option) if value == "" && c.compatSection != "" { value, _ = config.GetString(c.compatSection, option) if value != "" { - c.logger.Printf("WARNING: Configuring etcd option \"%s\" in section \"%s\" is deprecated, use section \"etcd\" instead", option, c.compatSection) + log.Printf("WARNING: Configuring etcd option \"%s\" in section \"%s\" is deprecated, use section \"etcd\" instead", option, c.compatSection) } } return value } -func (c *etcdClient) load(config *goconf.ConfigFile, ignoreErrors bool) error { +func (c *EtcdClient) load(config *goconf.ConfigFile, ignoreErrors bool) error { var endpoints []string if endpointsString := c.getConfigStringWithFallback(config, "endpoints"); endpointsString != "" { - endpoints = slices.Collect(internal.SplitEntries(endpointsString, ",")) + for _, ep := range strings.Split(endpointsString, ",") { + ep := strings.TrimSpace(ep) + if ep != "" { + endpoints = append(endpoints, ep) + } + } } else if discoverySrv := c.getConfigStringWithFallback(config, "discoverysrv"); discoverySrv != "" { discoveryService := c.getConfigStringWithFallback(config, "discoveryservice") clients, err := srv.GetClient("etcd-client", discoverySrv, discoveryService) if err != nil { if !ignoreErrors { - return fmt.Errorf("could not discover etcd endpoints for %s: %w", discoverySrv, err) + return fmt.Errorf("Could not discover etcd endpoints for %s: %w", discoverySrv, err) } } else { endpoints = clients.Endpoints @@ -122,7 +106,7 @@ func (c *etcdClient) load(config *goconf.ConfigFile, ignoreErrors bool) error { return nil } - c.logger.Printf("No etcd endpoints configured, not changing client") + log.Printf("No etcd endpoints configured, not changing client") } else { cfg := clientv3.Config{ Endpoints: endpoints, @@ -134,7 +118,7 @@ func (c *etcdClient) load(config *goconf.ConfigFile, ignoreErrors bool) error { if logLevel, _ := config.GetString("etcd", "loglevel"); logLevel != "" { var l zapcore.Level if err := l.Set(logLevel); err != nil { - return fmt.Errorf("unsupported etcd log level %s: %w", logLevel, err) + return fmt.Errorf("Unsupported etcd log level %s: %w", logLevel, err) } logConfig := zap.NewProductionConfig() @@ -154,10 +138,10 @@ func (c *etcdClient) load(config *goconf.ConfigFile, ignoreErrors bool) error { tlsConfig, err := tlsInfo.ClientConfig() if err != nil { if !ignoreErrors { - return fmt.Errorf("could not setup etcd TLS configuration: %w", err) + return fmt.Errorf("Could not setup etcd TLS configuration: %w", err) } - c.logger.Printf("Could not setup TLS configuration, will be disabled (%s)", err) + log.Printf("Could not setup TLS configuration, will be disabled (%s)", err) } else { cfg.TLS = tlsConfig } @@ -169,14 +153,14 @@ func (c *etcdClient) load(config *goconf.ConfigFile, ignoreErrors bool) error { return err } - c.logger.Printf("Could not create new client from etd endpoints %+v: %s", endpoints, err) + log.Printf("Could not create new client from etd endpoints %+v: %s", endpoints, err) } else { prev := c.getEtcdClient() if prev != nil { prev.Close() } c.client.Store(client) - c.logger.Printf("Using etcd endpoints %+v", endpoints) + log.Printf("Using etcd endpoints %+v", endpoints) c.notifyListeners() } } @@ -184,7 +168,7 @@ func (c *etcdClient) load(config *goconf.ConfigFile, ignoreErrors bool) error { return nil } -func (c *etcdClient) Close() error { +func (c *EtcdClient) Close() error { client := c.getEtcdClient() if client != nil { return client.Close() @@ -193,11 +177,11 @@ func (c *etcdClient) Close() error { return nil } -func (c *etcdClient) IsConfigured() bool { +func (c *EtcdClient) IsConfigured() bool { return c.getEtcdClient() != nil } -func (c *etcdClient) getEtcdClient() *clientv3.Client { +func (c *EtcdClient) getEtcdClient() *clientv3.Client { client := c.client.Load() if client == nil { return nil @@ -206,14 +190,14 @@ func (c *etcdClient) getEtcdClient() *clientv3.Client { return client.(*clientv3.Client) } -func (c *etcdClient) syncClient(ctx context.Context) error { +func (c *EtcdClient) syncClient(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() return c.getEtcdClient().Sync(ctx) } -func (c *etcdClient) notifyListeners() { +func (c *EtcdClient) notifyListeners() { c.mu.Lock() defer c.mu.Unlock() @@ -222,12 +206,12 @@ func (c *etcdClient) notifyListeners() { } } -func (c *etcdClient) AddListener(listener ClientListener) { +func (c *EtcdClient) AddListener(listener EtcdClientListener) { c.mu.Lock() defer c.mu.Unlock() if c.listeners == nil { - c.listeners = make(map[ClientListener]bool) + c.listeners = make(map[EtcdClientListener]bool) } c.listeners[listener] = true if client := c.getEtcdClient(); client != nil { @@ -235,15 +219,15 @@ func (c *etcdClient) AddListener(listener ClientListener) { } } -func (c *etcdClient) RemoveListener(listener ClientListener) { +func (c *EtcdClient) RemoveListener(listener EtcdClientListener) { c.mu.Lock() defer c.mu.Unlock() delete(c.listeners, listener) } -func (c *etcdClient) WaitForConnection(ctx context.Context) error { - backoff, err := async.NewExponentialBackoff(initialWaitDelay, maxWaitDelay) +func (c *EtcdClient) WaitForConnection(ctx context.Context) error { + backoff, err := NewExponentialBackoff(initialWaitDelay, maxWaitDelay) if err != nil { return err } @@ -257,29 +241,29 @@ func (c *etcdClient) WaitForConnection(ctx context.Context) error { if errors.Is(err, context.Canceled) { return err } else if errors.Is(err, context.DeadlineExceeded) { - c.logger.Printf("Timeout waiting for etcd client to connect to the cluster, retry in %s", backoff.NextWait()) + log.Printf("Timeout waiting for etcd client to connect to the cluster, retry in %s", backoff.NextWait()) } else { - c.logger.Printf("Could not sync etcd client with the cluster, retry in %s: %s", backoff.NextWait(), err) + log.Printf("Could not sync etcd client with the cluster, retry in %s: %s", backoff.NextWait(), err) } backoff.Wait(ctx) continue } - c.logger.Printf("Client synced, using endpoints %+v", c.getEtcdClient().Endpoints()) + log.Printf("Client synced, using endpoints %+v", c.getEtcdClient().Endpoints()) return nil } } -func (c *etcdClient) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { +func (c *EtcdClient) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { return c.getEtcdClient().Get(ctx, key, opts...) } -func (c *etcdClient) Watch(ctx context.Context, key string, nextRevision int64, watcher ClientWatcher, opts ...clientv3.OpOption) (int64, error) { - c.logger.Printf("Wait for leader and start watching on %s (rev=%d)", key, nextRevision) +func (c *EtcdClient) Watch(ctx context.Context, key string, nextRevision int64, watcher EtcdClientWatcher, opts ...clientv3.OpOption) (int64, error) { + log.Printf("Wait for leader and start watching on %s (rev=%d)", key, nextRevision) opts = append(opts, clientv3.WithRev(nextRevision), clientv3.WithPrevKV()) ch := c.getEtcdClient().Watch(clientv3.WithRequireLeader(ctx), key, opts...) - c.logger.Printf("Watch created for %s", key) + log.Printf("Watch created for %s", key) watcher.EtcdWatchCreated(c, key) for response := range ch { if err := response.Err(); err != nil { @@ -302,7 +286,7 @@ func (c *etcdClient) Watch(ctx context.Context, key string, nextRevision int64, } watcher.EtcdKeyDeleted(c, string(ev.Kv.Key), prevValue) default: - c.logger.Printf("Unsupported watch event %s %q -> %q", ev.Type, ev.Kv.Key, ev.Kv.Value) + log.Printf("Unsupported watch event %s %q -> %q", ev.Type, ev.Kv.Key, ev.Kv.Value) } } } diff --git a/etcd/client_test.go b/etcd_client_test.go similarity index 51% rename from etcd/client_test.go rename to etcd_client_test.go index f7bac1e..7986e2e 100644 --- a/etcd/client_test.go +++ b/etcd_client_test.go @@ -19,17 +19,17 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package etcd +package signaling import ( "context" - "crypto/rand" - "crypto/rsa" + "errors" "net" "net/url" "os" - "path" + "runtime" "strconv" + "syscall" "testing" "time" @@ -42,57 +42,41 @@ import ( "go.etcd.io/etcd/server/v3/lease" "go.uber.org/zap" "go.uber.org/zap/zaptest" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" -) - -const ( - testTimeout = 10 * time.Second ) var ( etcdListenUrl = "http://localhost:8080" ) -func NewEtcdForTestWithTls(t *testing.T, withTLS bool) (*embed.Etcd, string, string) { - t.Helper() +func isErrorAddressAlreadyInUse(err error) bool { + var eOsSyscall *os.SyscallError + if !errors.As(err, &eOsSyscall) { + return false + } + var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr) + if !errors.As(eOsSyscall, &errErrno) { + return false + } + if errErrno == syscall.EADDRINUSE { + return true + } + const WSAEADDRINUSE = 10048 + if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE { + return true + } + return false +} +func NewEtcdForTest(t *testing.T) *embed.Etcd { require := require.New(t) cfg := embed.NewConfig() cfg.Dir = t.TempDir() os.Chmod(cfg.Dir, 0700) // nolint cfg.LogLevel = "warn" - cfg.Name = "signalingtest" - cfg.ZapLoggerBuilder = embed.NewZapLoggerBuilder(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) u, err := url.Parse(etcdListenUrl) require.NoError(err) - var keyfile string - var certfile string - if withTLS { - u.Scheme = "https" - - tmpdir := t.TempDir() - key, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - keyfile = path.Join(tmpdir, "etcd.key") - require.NoError(internal.WritePrivateKey(key, keyfile)) - cfg.ClientTLSInfo.KeyFile = keyfile - cfg.PeerTLSInfo.KeyFile = keyfile - - cert := internal.GenerateSelfSignedCertificateForTesting(t, "etcd", key) - certfile = path.Join(tmpdir, "etcd.pem") - require.NoError(internal.WriteCertificate(cert, certfile)) - cfg.ClientTLSInfo.CertFile = certfile - cfg.ClientTLSInfo.TrustedCAFile = certfile - cfg.PeerTLSInfo.CertFile = certfile - cfg.PeerTLSInfo.TrustedCAFile = certfile - } - // Find a free port to bind the server to. var etcd *embed.Etcd for port := 50000; port < 50100; port++ { @@ -106,9 +90,10 @@ func NewEtcdForTestWithTls(t *testing.T, withTLS bool) (*embed.Etcd, string, str peerListener.Host = net.JoinHostPort("localhost", strconv.Itoa(port+2)) cfg.ListenPeerUrls = []url.URL{*peerListener} cfg.AdvertisePeerUrls = []url.URL{*peerListener} - cfg.InitialCluster = "signalingtest=" + peerListener.String() + cfg.InitialCluster = "default=" + peerListener.String() + cfg.ZapLoggerBuilder = embed.NewZapLoggerBuilder(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) etcd, err = embed.StartEtcd(cfg) - if test.IsErrorAddressAlreadyInUse(err) { + if isErrorAddressAlreadyInUse(err) { continue } @@ -124,25 +109,17 @@ func NewEtcdForTestWithTls(t *testing.T, withTLS bool) (*embed.Etcd, string, str // Wait for server to be ready. <-etcd.Server.ReadyNotify() - return etcd, keyfile, certfile -} - -func NewEtcdForTest(t *testing.T) *embed.Etcd { - t.Helper() - - etcd, _, _ := NewEtcdForTestWithTls(t, false) return etcd } -func NewClientForTest(t *testing.T) (*embed.Etcd, Client) { +func NewEtcdClientForTest(t *testing.T) (*embed.Etcd, *EtcdClient) { etcd := NewEtcdForTest(t) config := goconf.NewConfigFile() config.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String()) config.AddOption("etcd", "loglevel", "error") - logger := logtest.NewLoggerForTest(t) - client, err := NewClient(logger, config, "") + client, err := NewEtcdClient(config, "") require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, client.Close()) @@ -150,33 +127,14 @@ func NewClientForTest(t *testing.T) (*embed.Etcd, Client) { return etcd, client } -func NewEtcdClientWithTLSForTest(t *testing.T) (*embed.Etcd, Client) { - etcd, keyfile, certfile := NewEtcdForTestWithTls(t, true) - - config := goconf.NewConfigFile() - config.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String()) - config.AddOption("etcd", "loglevel", "error") - config.AddOption("etcd", "clientkey", keyfile) - config.AddOption("etcd", "clientcert", certfile) - config.AddOption("etcd", "cacert", certfile) - - logger := logtest.NewLoggerForTest(t) - client, err := NewClient(logger, config, "") - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, client.Close()) - }) - return etcd, client -} - -func SetValue(etcd *embed.Etcd, key string, value []byte) { +func SetEtcdValue(etcd *embed.Etcd, key string, value []byte) { if kv := etcd.Server.KV(); kv != nil { kv.Put([]byte(key), value, lease.NoLease) kv.Commit() } } -func DeleteValue(etcd *embed.Etcd, key string) { +func DeleteEtcdValue(etcd *embed.Etcd, key string) { if kv := etcd.Server.KV(); kv != nil { kv.DeleteRange([]byte(key), nil) kv.Commit() @@ -185,87 +143,17 @@ func DeleteValue(etcd *embed.Etcd, key string) { func Test_EtcdClient_Get(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) - require := require.New(t) - etcd, client := NewClientForTest(t) + etcd, client := NewEtcdClientForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - if info := client.GetServerInfoEtcd(); assert.NotNil(info) { - assert.NotEmpty(info.Active) - assert.Equal([]string{ - etcd.Config().ListenClientUrls[0].String(), - }, info.Endpoints) - assert.NotNil(info.Connected) - } - - require.NoError(client.WaitForConnection(ctx)) - - if info := client.GetServerInfoEtcd(); assert.NotNil(info) { - assert.NotEmpty(info.Active) - assert.Equal([]string{ - etcd.Config().ListenClientUrls[0].String(), - }, info.Endpoints) - if connected := info.Connected; assert.NotNil(connected) { - assert.True(*connected) - } - } - - if response, err := client.Get(ctx, "foo"); assert.NoError(err) { + if response, err := client.Get(context.Background(), "foo"); assert.NoError(err) { assert.EqualValues(0, response.Count) } - SetValue(etcd, "foo", []byte("bar")) + SetEtcdValue(etcd, "foo", []byte("bar")) - if response, err := client.Get(ctx, "foo"); assert.NoError(err) { - if assert.EqualValues(1, response.Count) { - assert.Equal("foo", string(response.Kvs[0].Key)) - assert.Equal("bar", string(response.Kvs[0].Value)) - } - } -} - -func Test_EtcdClientTLS_Get(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - assert := assert.New(t) - require := require.New(t) - etcd, client := NewEtcdClientWithTLSForTest(t) - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - if info := client.GetServerInfoEtcd(); assert.NotNil(info) { - assert.NotEmpty(info.Active) - assert.Equal([]string{ - etcd.Config().ListenClientUrls[0].String(), - }, info.Endpoints) - assert.NotNil(info.Connected) - } - - require.NoError(client.WaitForConnection(ctx)) - - if info := client.GetServerInfoEtcd(); assert.NotNil(info) { - assert.NotEmpty(info.Active) - assert.Equal([]string{ - etcd.Config().ListenClientUrls[0].String(), - }, info.Endpoints) - if connected := info.Connected; assert.NotNil(connected) { - assert.True(*connected) - } - } - - if response, err := client.Get(ctx, "foo"); assert.NoError(err) { - assert.EqualValues(0, response.Count) - } - - SetValue(etcd, "foo", []byte("bar")) - - if response, err := client.Get(ctx, "foo"); assert.NoError(err) { + if response, err := client.Get(context.Background(), "foo"); assert.NoError(err) { if assert.EqualValues(1, response.Count) { assert.Equal("foo", string(response.Kvs[0].Key)) assert.Equal("bar", string(response.Kvs[0].Value)) @@ -275,20 +163,19 @@ func Test_EtcdClientTLS_Get(t *testing.T) { func Test_EtcdClient_GetPrefix(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) - etcd, client := NewClientForTest(t) + etcd, client := NewEtcdClientForTest(t) - if response, err := client.Get(ctx, "foo"); assert.NoError(err) { + if response, err := client.Get(context.Background(), "foo"); assert.NoError(err) { assert.EqualValues(0, response.Count) } - SetValue(etcd, "foo", []byte("1")) - SetValue(etcd, "foo/lala", []byte("2")) - SetValue(etcd, "lala/foo", []byte("3")) + SetEtcdValue(etcd, "foo", []byte("1")) + SetEtcdValue(etcd, "foo/lala", []byte("2")) + SetEtcdValue(etcd, "lala/foo", []byte("3")) - if response, err := client.Get(ctx, "foo", clientv3.WithPrefix()); assert.NoError(err) { + if response, err := client.Get(context.Background(), "foo", clientv3.WithPrefix()); assert.NoError(err) { if assert.EqualValues(2, response.Count) { assert.Equal("foo", string(response.Kvs[0].Key)) assert.Equal("1", string(response.Kvs[0].Value)) @@ -333,7 +220,7 @@ func (l *EtcdClientTestListener) Close() { l.cancel() } -func (l *EtcdClientTestListener) EtcdClientCreated(client Client) { +func (l *EtcdClientTestListener) EtcdClientCreated(client *EtcdClient) { go func() { assert := assert.New(l.t) if err := client.WaitForConnection(l.ctx); !assert.NoError(err) { @@ -359,10 +246,10 @@ func (l *EtcdClientTestListener) EtcdClientCreated(client Client) { }() } -func (l *EtcdClientTestListener) EtcdWatchCreated(client Client, key string) { +func (l *EtcdClientTestListener) EtcdWatchCreated(client *EtcdClient, key string) { } -func (l *EtcdClientTestListener) EtcdKeyUpdated(client Client, key string, value []byte, prevValue []byte) { +func (l *EtcdClientTestListener) EtcdKeyUpdated(client *EtcdClient, key string, value []byte, prevValue []byte) { evt := etcdEvent{ t: clientv3.EventTypePut, key: string(key), @@ -374,7 +261,7 @@ func (l *EtcdClientTestListener) EtcdKeyUpdated(client Client, key string, value l.events <- evt } -func (l *EtcdClientTestListener) EtcdKeyDeleted(client Client, key string, prevValue []byte) { +func (l *EtcdClientTestListener) EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte) { evt := etcdEvent{ t: clientv3.EventTypeDelete, key: string(key), @@ -387,14 +274,13 @@ func (l *EtcdClientTestListener) EtcdKeyDeleted(client Client, key string, prevV func Test_EtcdClient_Watch(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) - etcd, client := NewClientForTest(t) + etcd, client := NewEtcdClientForTest(t) - SetValue(etcd, "foo/a", []byte("1")) + SetEtcdValue(etcd, "foo/a", []byte("1")) - listener := NewEtcdClientTestListener(ctx, t) + listener := NewEtcdClientTestListener(context.Background(), t) defer listener.Close() client.AddListener(listener) @@ -402,19 +288,19 @@ func Test_EtcdClient_Watch(t *testing.T) { <-listener.initial - SetValue(etcd, "foo/b", []byte("2")) + SetEtcdValue(etcd, "foo/b", []byte("2")) event := <-listener.events assert.Equal(clientv3.EventTypePut, event.t) assert.Equal("foo/b", event.key) assert.Equal("2", event.value) - SetValue(etcd, "foo/a", []byte("3")) + SetEtcdValue(etcd, "foo/a", []byte("3")) event = <-listener.events assert.Equal(clientv3.EventTypePut, event.t) assert.Equal("foo/a", event.key) assert.Equal("3", event.value) - DeleteValue(etcd, "foo/a") + DeleteEtcdValue(etcd, "foo/a") event = <-listener.events assert.Equal(clientv3.EventTypeDelete, event.t) assert.Equal("foo/a", event.key) diff --git a/server/federation.go b/federation.go similarity index 58% rename from server/federation.go rename to federation.go index 0c3dbb7..9b609c8 100644 --- a/server/federation.go +++ b/federation.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" @@ -27,6 +27,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "net" "strconv" "strings" @@ -35,35 +36,16 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/mailru/easyjson" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" + easyjson "github.com/mailru/easyjson" ) const ( - // Time allowed to write a message to the peer. - writeWait = 10 * time.Second - - // Time allowed to read the next pong message from the peer. - pongWait = 60 * time.Second - - // Send pings to peer with this period. Must be less than pongWait. - pingPeriod = (pongWait * 9) / 10 - - // Maximum message size allowed from peer. - maxMessageSize = 64 * 1024 - initialFederationReconnectInterval = 100 * time.Millisecond maxFederationReconnectInterval = 8 * time.Second ) var ( - ErrNotConnected = errors.New("not connected") - ErrFederationNotSupported = api.NewError("federation_unsupported", "The target server does not support federation.") - - federationWriteBufferPool = &sync.Pool{} + ErrFederationNotSupported = NewError("federation_unsupported", "The target server does not support federation.") ) func isClosedError(err error) bool { @@ -73,69 +55,54 @@ func isClosedError(err error) bool { strings.Contains(err.Error(), net.ErrClosed.Error()) } -func getCloudUrlWithoutPath(s string) string { +func getCloudUrl(s string) string { + if strings.HasPrefix(s, "https://") { + s = s[8:] + } else { + s = strings.TrimPrefix(s, "http://") + } if pos := strings.Index(s, "/ocs/v"); pos != -1 { s = s[:pos] - } else { - s = strings.TrimSuffix(s, "/") } return s } -func getCloudUrl(s string) string { - var found bool - if s, found = strings.CutPrefix(s, "https://"); !found { - s = strings.TrimPrefix(s, "http://") - } - return getCloudUrlWithoutPath(s) -} - type FederationClient struct { - logger log.Logger hub *Hub session *ClientSession - message atomic.Pointer[api.ClientMessage] + message atomic.Pointer[ClientMessage] roomId atomic.Value remoteRoomId atomic.Value changeRoomId atomic.Bool - federation atomic.Pointer[api.RoomFederationMessage] + federation atomic.Pointer[RoomFederationMessage] - mu sync.Mutex - dialer *websocket.Dialer - url string - // +checklocks:mu - conn *websocket.Conn - closer *internal.Closer - // +checklocks:mu + mu sync.Mutex + dialer *websocket.Dialer + url string + conn *websocket.Conn + closer *Closer reconnectDelay time.Duration - reconnecting atomic.Bool - // +checklocks:mu - reconnectFunc *time.Timer + reconnecting bool + reconnectFunc *time.Timer - helloMu sync.Mutex - // +checklocks:helloMu + helloMu sync.Mutex helloMsgId string - // +checklocks:helloMu - helloAuth *api.FederationAuthParams - // +checklocks:helloMu - resumeId api.PrivateSessionId - hello atomic.Pointer[api.HelloServerMessage] + helloAuth *FederationAuthParams + resumeId string + hello atomic.Pointer[HelloServerMessage] - // +checklocks:helloMu - pendingMessages []*api.ClientMessage + pendingMessages []*ClientMessage closeOnLeave atomic.Bool } -func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, message *api.ClientMessage) (*FederationClient, error) { +func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, message *ClientMessage) (*FederationClient, error) { if message.Type != "room" || message.Room == nil || message.Room.Federation == nil { return nil, fmt.Errorf("expected federation room message, got %+v", message) } - dialer := &websocket.Dialer{ - WriteBufferPool: federationWriteBufferPool, - } + var dialer websocket.Dialer if hub.skipFederationVerify { dialer.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, @@ -143,7 +110,7 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, } room := message.Room - u := *room.Federation.ParsedSignalingUrl + u := *room.Federation.parsedSignalingUrl switch u.Scheme { case "http": u.Scheme = "ws" @@ -158,15 +125,14 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, } result := &FederationClient{ - logger: hub.logger, hub: hub, session: session, reconnectDelay: initialFederationReconnectInterval, - dialer: dialer, + dialer: &dialer, url: url, - closer: internal.NewCloser(), + closer: NewCloser(), } result.roomId.Store(room.RoomId) result.remoteRoomId.Store(remoteRoomId) @@ -188,19 +154,8 @@ func NewFederationClient(ctx context.Context, hub *Hub, session *ClientSession, return result, nil } -func (c *FederationClient) LocalAddr() net.Addr { - c.mu.Lock() - defer c.mu.Unlock() - - if c.conn == nil { - return nil - } - - return c.conn.LocalAddr() -} - func (c *FederationClient) URL() string { - return c.federation.Load().ParsedSignalingUrl.String() + return c.federation.Load().parsedSignalingUrl.String() } func (c *FederationClient) RoomId() string { @@ -211,14 +166,14 @@ func (c *FederationClient) RemoteRoomId() string { return c.remoteRoomId.Load().(string) } -func (c *FederationClient) CanReuse(federation *api.RoomFederationMessage) bool { +func (c *FederationClient) CanReuse(federation *RoomFederationMessage) bool { fed := c.federation.Load() return fed.NextcloudUrl == federation.NextcloudUrl && fed.SignalingUrl == federation.SignalingUrl } func (c *FederationClient) connect(ctx context.Context) error { - c.logger.Printf("Creating federation connection to %s for %s", c.URL(), c.session.PublicId()) + log.Printf("Creating federation connection to %s for %s", c.URL(), c.session.PublicId()) conn, response, err := c.dialer.DialContext(ctx, c.url, nil) if err != nil { return err @@ -228,20 +183,20 @@ func (c *FederationClient) connect(ctx context.Context) error { supportsFederation := false for _, f := range features { f = strings.TrimSpace(f) - if f == api.ServerFeatureFederation { + if f == ServerFeatureFederation { supportsFederation = true break } } if !supportsFederation { if err := conn.Close(); err != nil { - c.logger.Printf("Error closing federation connection to %s: %s", c.URL(), err) + log.Printf("Error closing federation connection to %s: %s", c.URL(), err) } return ErrFederationNotSupported } - c.logger.Printf("Federation connection established to %s for %s", c.URL(), c.session.PublicId()) + log.Printf("Federation connection established to %s for %s", c.URL(), c.session.PublicId()) c.mu.Lock() defer c.mu.Unlock() @@ -263,7 +218,7 @@ func (c *FederationClient) connect(ctx context.Context) error { return nil } -func (c *FederationClient) ChangeRoom(message *api.ClientMessage) error { +func (c *FederationClient) ChangeRoom(message *ClientMessage) error { if message.Room == nil || message.Room.Federation == nil { return fmt.Errorf("expected federation room message, got %+v", message) } else if !c.CanReuse(message.Room.Federation) { @@ -274,33 +229,29 @@ func (c *FederationClient) ChangeRoom(message *api.ClientMessage) error { return c.joinRoom() } -func (c *FederationClient) Leave(message *api.ClientMessage) error { +func (c *FederationClient) Leave(message *ClientMessage) error { c.mu.Lock() defer c.mu.Unlock() if message == nil { - message = &api.ClientMessage{ + message = &ClientMessage{ Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: "", }, } } - c.closeOnLeave.Store(true) - if err := c.sendMessageLocked(message); err != nil { - c.closeOnLeave.Store(false) - if !errors.Is(err, websocket.ErrCloseSent) { - return err - } + if err := c.sendMessageLocked(message); err != nil && !errors.Is(err, websocket.ErrCloseSent) { + return err } + c.closeOnLeave.Store(true) return nil } func (c *FederationClient) Close() { c.closer.Close() - c.hub.removeFederationClient(c) c.mu.Lock() defer c.mu.Unlock() @@ -308,28 +259,27 @@ func (c *FederationClient) Close() { c.closeConnection(true) } -// +checklocks:c.mu func (c *FederationClient) closeConnection(withBye bool) { if c.conn == nil { return } if withBye { - if err := c.sendMessageLocked(&api.ClientMessage{ + if err := c.sendMessageLocked(&ClientMessage{ Type: "bye", }); err != nil && !isClosedError(err) { - c.logger.Printf("Error sending bye on federation connection to %s: %s", c.URL(), err) + log.Printf("Error sending bye on federation connection to %s: %s", c.URL(), err) } } closeMessage := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") deadline := time.Now().Add(writeWait) if err := c.conn.WriteControl(websocket.CloseMessage, closeMessage, deadline); err != nil && !isClosedError(err) { - c.logger.Printf("Error sending close message on federation connection to %s: %s", c.URL(), err) + log.Printf("Error sending close message on federation connection to %s: %s", c.URL(), err) } if err := c.conn.Close(); err != nil && !isClosedError(err) { - c.logger.Printf("Error closing federation connection to %s: %s", c.URL(), err) + log.Printf("Error closing federation connection to %s: %s", c.URL(), err) } c.conn = nil @@ -348,13 +298,12 @@ func (c *FederationClient) scheduleReconnect() { c.scheduleReconnectLocked() } -// +checklocks:c.mu func (c *FederationClient) scheduleReconnectLocked() { - c.reconnecting.Store(true) + c.reconnecting = true if c.hello.Swap(nil) != nil { - c.session.SendMessage(&api.ServerMessage{ + c.session.SendMessage(&ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "federation_interrupted", }, @@ -377,12 +326,11 @@ func (c *FederationClient) reconnect() { return } - ctx := log.NewLoggerContext(context.Background(), c.logger) - ctx, cancel := context.WithTimeout(ctx, time.Duration(c.hub.federationTimeout)) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.hub.federationTimeout)) defer cancel() if err := c.connect(ctx); err != nil { - c.logger.Printf("Error connecting to federation server %s for %s: %s", c.URL(), c.session.PublicId(), err) + log.Printf("Error connecting to federation server %s for %s: %s", c.URL(), c.session.PublicId(), err) c.scheduleReconnect() return } @@ -406,7 +354,7 @@ func (c *FederationClient) readPump(conn *websocket.Conn) { } if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { - c.logger.Printf("Error reading from %s for %s: %s", c.URL(), c.session.PublicId(), err) + log.Printf("Error reading from %s for %s: %s", c.URL(), c.session.PublicId(), err) } c.scheduleReconnect() @@ -417,9 +365,9 @@ func (c *FederationClient) readPump(conn *websocket.Conn) { continue } - var msg api.ServerMessage + var msg ServerMessage if err := json.Unmarshal(data, &msg); err != nil { - c.logger.Printf("Error unmarshalling %s from %s: %s", string(data), c.URL(), err) + log.Printf("Error unmarshalling %s from %s: %s", string(data), c.URL(), err) continue } @@ -448,7 +396,7 @@ func (c *FederationClient) sendPing() { msg := strconv.FormatInt(now, 10) c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil { - c.logger.Printf("Could not send ping to federated client %s for %s: %v", c.URL(), c.session.PublicId(), err) + log.Printf("Could not send ping to federated client %s for %s: %v", c.URL(), c.session.PublicId(), err) c.scheduleReconnectLocked() } } @@ -469,9 +417,9 @@ func (c *FederationClient) writePump() { func (c *FederationClient) closeWithError(err error) { c.Close() - e, ok := internal.AsErrorType[*api.Error](err) - if !ok { - e = api.NewError("federation_error", err.Error()) + var e *Error + if !errors.As(err, &e) { + e = NewError("federation_error", err.Error()) } var id string @@ -479,23 +427,22 @@ func (c *FederationClient) closeWithError(err error) { id = message.Id } - c.session.SendMessage(&api.ServerMessage{ + c.session.SendMessage(&ServerMessage{ Id: id, Type: "error", Error: e, }) } -func (c *FederationClient) sendHello(auth *api.FederationAuthParams) error { +func (c *FederationClient) sendHello(auth *FederationAuthParams) error { c.helloMu.Lock() defer c.helloMu.Unlock() return c.sendHelloLocked(auth) } -// +checklocks:c.helloMu -func (c *FederationClient) sendHelloLocked(auth *api.FederationAuthParams) error { - c.helloMsgId = internal.RandomString(8) +func (c *FederationClient) sendHelloLocked(auth *FederationAuthParams) error { + c.helloMsgId = newRandomString(8) authData, err := json.Marshal(auth) if err != nil { @@ -503,19 +450,19 @@ func (c *FederationClient) sendHelloLocked(auth *api.FederationAuthParams) error } c.helloAuth = auth - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: c.helloMsgId, Type: "hello", - Hello: &api.HelloClientMessage{ - Version: api.HelloVersionV2, + Hello: &HelloClientMessage{ + Version: HelloVersionV2, Features: c.session.GetFeatures(), }, } if resumeId := c.resumeId; resumeId != "" { msg.Hello.ResumeId = resumeId } else { - msg.Hello.Auth = &api.HelloClientMessageAuth{ - Type: api.HelloClientTypeFederation, + msg.Hello.Auth = &HelloClientMessageAuth{ + Type: HelloClientTypeFederation, Url: c.federation.Load().NextcloudUrl, Params: authData, } @@ -523,29 +470,29 @@ func (c *FederationClient) sendHelloLocked(auth *api.FederationAuthParams) error return c.SendMessage(msg) } -func (c *FederationClient) processWelcome(msg *api.ServerMessage) { - if !msg.Welcome.HasFeature(api.ServerFeatureFederation) { +func (c *FederationClient) processWelcome(msg *ServerMessage) { + if !msg.Welcome.HasFeature(ServerFeatureFederation) { c.closeWithError(ErrFederationNotSupported) return } - federationParams := &api.FederationAuthParams{ + federationParams := &FederationAuthParams{ Token: c.federation.Load().Token, } if err := c.sendHello(federationParams); err != nil { - c.logger.Printf("Error sending hello message to %s for %s: %s", c.URL(), c.session.PublicId(), err) + log.Printf("Error sending hello message to %s for %s: %s", c.URL(), c.session.PublicId(), err) c.closeWithError(err) } } -func (c *FederationClient) processHello(msg *api.ServerMessage) { +func (c *FederationClient) processHello(msg *ServerMessage) { c.resetReconnect() c.helloMu.Lock() defer c.helloMu.Unlock() if msg.Id != c.helloMsgId { - c.logger.Printf("Received hello response %+v for unknown request, expected %s", msg, c.helloMsgId) + log.Printf("Received hello response %+v for unknown request, expected %s", msg, c.helloMsgId) if err := c.sendHelloLocked(c.helloAuth); err != nil { c.closeWithError(err) } @@ -564,12 +511,12 @@ func (c *FederationClient) processHello(msg *api.ServerMessage) { c.closeWithError(err) } default: - c.logger.Printf("Received hello error from federated client for %s to %s: %+v", c.session.PublicId(), c.URL(), msg) + log.Printf("Received hello error from federated client for %s to %s: %+v", c.session.PublicId(), c.URL(), msg) c.closeWithError(msg.Error) } return } else if msg.Type != "hello" { - c.logger.Printf("Received unknown hello response from federated client for %s to %s: %+v", c.session.PublicId(), c.URL(), msg) + log.Printf("Received unknown hello response from federated client for %s to %s: %+v", c.session.PublicId(), c.URL(), msg) if err := c.sendHelloLocked(c.helloAuth); err != nil { c.closeWithError(err) } @@ -579,13 +526,13 @@ func (c *FederationClient) processHello(msg *api.ServerMessage) { c.hello.Store(msg.Hello) if c.resumeId == "" { c.resumeId = msg.Hello.ResumeId - if c.reconnecting.Load() { - c.session.SendMessage(&api.ServerMessage{ + if c.reconnecting { + c.session.SendMessage(&ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "federation_resumed", - Resumed: internal.MakePtr(false), + Resumed: makePtr(false), }, }) // Setting the federation client will reset any information on previously @@ -597,12 +544,12 @@ func (c *FederationClient) processHello(msg *api.ServerMessage) { c.closeWithError(err) } } else { - c.session.SendMessage(&api.ServerMessage{ + c.session.SendMessage(&ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "federation_resumed", - Resumed: internal.MakePtr(true), + Resumed: makePtr(true), }, }) @@ -610,7 +557,7 @@ func (c *FederationClient) processHello(msg *api.ServerMessage) { messages := c.pendingMessages c.pendingMessages = nil - c.logger.Printf("Sending %d pending messages to %s for %s", count, c.URL(), c.session.PublicId()) + log.Printf("Sending %d pending messages to %s for %s", count, c.URL(), c.session.PublicId()) c.helloMu.Unlock() defer c.helloMu.Lock() @@ -619,7 +566,7 @@ func (c *FederationClient) processHello(msg *api.ServerMessage) { defer c.mu.Unlock() for _, msg := range messages { if err := c.sendMessageLocked(msg); err != nil { - c.logger.Printf("Error sending pending message %+v on federation connection to %s: %s", msg, c.URL(), err) + log.Printf("Error sending pending message %+v on federation connection to %s: %s", msg, c.URL(), err) break } } @@ -640,105 +587,43 @@ func (c *FederationClient) joinRoom() error { remoteRoomId = room.RoomId } - return c.SendMessage(&api.ClientMessage{ + return c.SendMessage(&ClientMessage{ Id: message.Id, Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: remoteRoomId, SessionId: room.SessionId, }, }) } -func (c *FederationClient) updateActor(u api.StringMap, actorIdKey, actorTypeKey string, localCloudUrl string, remoteCloudUrl string) (changed bool) { - if actorType, found := api.GetStringMapEntry[string](u, actorTypeKey); found { - if actorId, found := api.GetStringMapEntry[string](u, actorIdKey); found { - switch actorType { - case api.ActorTypeFederatedUsers: - if strings.HasSuffix(actorId, localCloudUrl) { - u[actorIdKey] = actorId[:len(actorId)-len(localCloudUrl)] - u[actorTypeKey] = api.ActorTypeUsers - changed = true - } - case api.ActorTypeUsers: - u[actorIdKey] = actorId + remoteCloudUrl - u[actorTypeKey] = api.ActorTypeFederatedUsers - changed = true - } - } - } - return -} - -func (c *FederationClient) updateComment(comment api.StringMap, localCloudUrl string, remoteCloudUrl string) bool { - changed := c.updateActor(comment, "actorId", "actorType", localCloudUrl, remoteCloudUrl) - if c.updateActor(comment, "lastEditActorId", "lastEditActorType", localCloudUrl, remoteCloudUrl) { - changed = true - } - - if token, found := api.GetStringMapString[string](comment, "token"); found && c.changeRoomId.Load() && token == c.RemoteRoomId() { - comment["token"] = c.RoomId() - changed = true - } - - if params, found := api.GetStringMapEntry[map[string]any](comment, "messageParameters"); found { - localUrl := getCloudUrlWithoutPath(c.session.BackendUrl()) - remoteUrl := getCloudUrlWithoutPath(c.federation.Load().NextcloudUrl) - for key, paramOb := range params { - if !strings.HasPrefix(key, "mention-") { - // Only need to process mentions. - continue - } - - param, ok := api.ConvertStringMap(paramOb) - if !ok { - continue - } - - if ptype, found := api.GetStringMapString[string](param, "type"); found { - switch ptype { - case "user": - if server, found := api.GetStringMapString[string](param, "server"); found && server == localUrl { - delete(param, "server") - params[key] = param - changed = true - continue - } - - if _, found := api.GetStringMapString[string](param, "mention-id"); !found { - param["mention-id"] = param["id"] - param["server"] = remoteUrl - params[key] = param - changed = true - continue - } - case "call": - roomId := c.RoomId() - remoteRoomId := c.RemoteRoomId() - // TODO: Should we also rewrite the room avatar url in "icon-url"? - if c.changeRoomId.Load() && param["id"] == remoteRoomId { - param["id"] = roomId - } - } - } - } - } - return changed -} - -func (c *FederationClient) updateEventUsers(users []api.StringMap, localSessionId api.PublicSessionId, remoteSessionId api.PublicSessionId) { +func (c *FederationClient) updateEventUsers(users []map[string]interface{}, localSessionId string, remoteSessionId string) { localCloudUrl := "@" + getCloudUrl(c.session.BackendUrl()) + localCloudUrlLen := len(localCloudUrl) remoteCloudUrl := "@" + getCloudUrl(c.federation.Load().NextcloudUrl) checkSessionId := true for _, u := range users { - c.updateActor(u, "actorId", "actorType", localCloudUrl, remoteCloudUrl) + if actorType, found := getStringMapEntry[string](u, "actorType"); found { + if actorId, found := getStringMapEntry[string](u, "actorId"); found { + switch actorType { + case ActorTypeFederatedUsers: + if strings.HasSuffix(actorId, localCloudUrl) { + u["actorId"] = actorId[:len(actorId)-localCloudUrlLen] + u["actorType"] = ActorTypeUsers + } + case ActorTypeUsers: + u["actorId"] = actorId + remoteCloudUrl + u["actorType"] = ActorTypeFederatedUsers + } + } + } if checkSessionId { key := "sessionId" - sid, found := api.GetStringMapString[api.PublicSessionId](u, key) + sid, found := getStringMapEntry[string](u, key) if !found { key := "sessionid" - sid, found = api.GetStringMapString[api.PublicSessionId](u, key) + sid, found = getStringMapEntry[string](u, key) } if found && sid == remoteSessionId { u[key] = localSessionId @@ -748,21 +633,21 @@ func (c *FederationClient) updateEventUsers(users []api.StringMap, localSessionI } } -func (c *FederationClient) updateSessionRecipient(recipient *api.MessageClientMessageRecipient, localSessionId api.PublicSessionId, remoteSessionId api.PublicSessionId) { - if recipient != nil && recipient.Type == api.RecipientTypeSession && remoteSessionId != "" && recipient.SessionId == remoteSessionId { +func (c *FederationClient) updateSessionRecipient(recipient *MessageClientMessageRecipient, localSessionId string, remoteSessionId string) { + if recipient != nil && recipient.Type == RecipientTypeSession && remoteSessionId != "" && recipient.SessionId == remoteSessionId { recipient.SessionId = localSessionId } } -func (c *FederationClient) updateSessionSender(sender *api.MessageServerMessageSender, localSessionId api.PublicSessionId, remoteSessionId api.PublicSessionId) { - if sender != nil && sender.Type == api.RecipientTypeSession && remoteSessionId != "" && sender.SessionId == remoteSessionId { +func (c *FederationClient) updateSessionSender(sender *MessageServerMessageSender, localSessionId string, remoteSessionId string) { + if sender != nil && sender.Type == RecipientTypeSession && remoteSessionId != "" && sender.SessionId == remoteSessionId { sender.SessionId = localSessionId } } -func (c *FederationClient) processMessage(msg *api.ServerMessage) { +func (c *FederationClient) processMessage(msg *ServerMessage) { localSessionId := c.session.PublicId() - var remoteSessionId api.PublicSessionId + var remoteSessionId string if hello := c.hello.Load(); hello != nil { remoteSessionId = hello.SessionId } @@ -777,10 +662,10 @@ func (c *FederationClient) processMessage(msg *api.ServerMessage) { c.updateSessionSender(msg.Control.Sender, localSessionId, remoteSessionId) // Special handling for "forceMute" event. if len(msg.Control.Data) > 0 && msg.Control.Data[0] == '{' { - var data api.StringMap + var data map[string]interface{} if err := json.Unmarshal(msg.Control.Data, &data); err == nil { if action, found := data["action"]; found && action == "forceMute" { - if peerId, found := api.GetStringMapString[api.PublicSessionId](data, "peerId"); found && peerId == remoteSessionId { + if peerId, found := data["peerId"]; found && peerId == remoteSessionId { data["peerId"] = localSessionId if d, err := json.Marshal(data); err == nil { msg.Control.Data = d @@ -817,10 +702,9 @@ func (c *FederationClient) processMessage(msg *api.ServerMessage) { switch msg.Event.Type { case "join": if remoteSessionId != "" { - for idx, j := range msg.Event.Join { + for _, j := range msg.Event.Join { if j.SessionId == remoteSessionId { j.SessionId = localSessionId - msg.Event.Join[idx] = j break } } @@ -841,32 +725,6 @@ func (c *FederationClient) processMessage(msg *api.ServerMessage) { if c.changeRoomId.Load() && msg.Event.Message.RoomId == remoteRoomId { msg.Event.Message.RoomId = roomId } - if msg.Event.Type == "message" && msg.Event.Message != nil { - if data, err := msg.Event.Message.GetData(); err == nil { - if data.Type == "chat" && data.Chat != nil && len(data.Chat.Comment) > 0 { - var comment api.StringMap - if err := json.Unmarshal(data.Chat.Comment, &comment); err == nil { - localCloudUrl := "@" + getCloudUrl(c.session.BackendUrl()) - remoteCloudUrl := "@" + getCloudUrl(c.federation.Load().NextcloudUrl) - changed := c.updateComment(comment, localCloudUrl, remoteCloudUrl) - if parent, found := comment.GetStringMap("parent"); found { - if c.updateComment(parent, localCloudUrl, remoteCloudUrl) { - comment["parent"] = parent - changed = true - } - } - if changed { - if encoded, err := json.Marshal(comment); err == nil { - data.Chat.Comment = encoded - if encoded, err = json.Marshal(data); err == nil { - msg.Event.Message.Data = encoded - } - } - } - } - } - } - } } case "roomlist": switch msg.Event.Type { @@ -887,7 +745,7 @@ func (c *FederationClient) processMessage(msg *api.ServerMessage) { case "error": if c.changeRoomId.Load() && msg.Error.Code == "already_joined" { if len(msg.Error.Details) > 0 { - var details api.RoomErrorDetails + var details RoomErrorDetails if err := json.Unmarshal(msg.Error.Details, &details); err == nil && details.Room != nil { if details.Room.RoomId == remoteRoomId { details.Room.RoomId = roomId @@ -929,7 +787,7 @@ func (c *FederationClient) processMessage(msg *api.ServerMessage) { c.updateSessionRecipient(msg.Message.Recipient, localSessionId, remoteSessionId) c.updateSessionSender(msg.Message.Sender, localSessionId, remoteSessionId) if remoteSessionId != "" && len(msg.Message.Data) > 0 { - var ao api.AnswerOfferMessage + var ao AnswerOfferMessage if json.Unmarshal(msg.Message.Data, &ao) == nil && (ao.Type == "offer" || ao.Type == "answer") { changed := false if ao.From == remoteSessionId { @@ -948,10 +806,6 @@ func (c *FederationClient) processMessage(msg *api.ServerMessage) { } } } - case "transient": - if remoteSessionId != "" && msg.TransientData != nil && msg.TransientData.Key == api.TransientSessionDataPrefix+string(remoteSessionId) { - msg.TransientData.Key = api.TransientSessionDataPrefix + string(localSessionId) - } } c.session.SendMessage(msg) @@ -960,31 +814,25 @@ func (c *FederationClient) processMessage(msg *api.ServerMessage) { } } -func (c *FederationClient) ProxyMessage(message *api.ClientMessage) error { +func (c *FederationClient) ProxyMessage(message *ClientMessage) error { switch message.Type { case "message": if hello := c.hello.Load(); hello != nil { c.updateSessionRecipient(&message.Message.Recipient, hello.SessionId, c.session.PublicId()) } - case "transient": - if hello := c.hello.Load(); hello != nil { - if message.TransientData != nil && message.TransientData.Key == api.TransientSessionDataPrefix+string(c.session.PublicId()) { - message.TransientData.Key = api.TransientSessionDataPrefix + string(hello.SessionId) - } - } } return c.SendMessage(message) } -func (c *FederationClient) SendMessage(message *api.ClientMessage) error { +func (c *FederationClient) SendMessage(message *ClientMessage) error { c.mu.Lock() defer c.mu.Unlock() return c.sendMessageLocked(message) } -func (c *FederationClient) deferMessage(message *api.ClientMessage) { +func (c *FederationClient) deferMessage(message *ClientMessage) { c.helloMu.Lock() defer c.helloMu.Unlock() if c.resumeId == "" { @@ -993,12 +841,11 @@ func (c *FederationClient) deferMessage(message *api.ClientMessage) { c.pendingMessages = append(c.pendingMessages, message) if len(c.pendingMessages) >= warnPendingMessagesCount { - c.logger.Printf("Session %s has %d pending federated messages", c.session.PublicId(), len(c.pendingMessages)) + log.Printf("Session %s has %d pending federated messages", c.session.PublicId(), len(c.pendingMessages)) } } -// +checklocks:c.mu -func (c *FederationClient) sendMessageLocked(message *api.ClientMessage) error { +func (c *FederationClient) sendMessageLocked(message *ClientMessage) error { if c.conn == nil { if message.Type != "room" { // Join requests will be automatically sent after the hello response has @@ -1011,7 +858,7 @@ func (c *FederationClient) sendMessageLocked(message *api.ClientMessage) error { c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint writer, err := c.conn.NextWriter(websocket.TextMessage) if err == nil { - if m, ok := (any(message)).(easyjson.Marshaler); ok { + if m, ok := (interface{}(message)).(easyjson.Marshaler); ok { _, err = easyjson.MarshalToWriter(m, writer) } else { err = json.NewEncoder(writer).Encode(message) @@ -1026,7 +873,7 @@ func (c *FederationClient) sendMessageLocked(message *api.ClientMessage) error { return err } - c.logger.Printf("Could not send message %+v for %s to federated client %s: %v", message, c.session.PublicId(), c.URL(), err) + log.Printf("Could not send message %+v for %s to federated client %s: %v", message, c.session.PublicId(), c.URL(), err) c.deferMessage(message) c.scheduleReconnectLocked() } diff --git a/server/federation_test.go b/federation_test.go similarity index 57% rename from server/federation_test.go rename to federation_test.go index 5c92364..c185b5c 100644 --- a/server/federation_test.go +++ b/federation_test.go @@ -19,26 +19,22 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" "encoding/json" - "fmt" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/test" ) func Test_FederationInvalidToken(t *testing.T) { - t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) require := require.New(t) @@ -51,15 +47,16 @@ func Test_FederationInvalidToken(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - MustSucceed1(t, client.RunUntilHello, ctx) + _, err := client.RunUntilHello(ctx) + require.NoError(err) - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "join-room-fed", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: "test-room", SessionId: "room-session-id", - Federation: &api.RoomFederationMessage{ + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, Token: "invalid-token", @@ -68,7 +65,7 @@ func Test_FederationInvalidToken(t *testing.T) { } require.NoError(client.WriteJSON(msg)) - if message, ok := client.RunUntilMessage(ctx); ok { + if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("error", message.Type) require.Equal("invalid_token", message.Error.Code) @@ -76,7 +73,8 @@ func Test_FederationInvalidToken(t *testing.T) { } func Test_Federation(t *testing.T) { - t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) require := require.New(t) @@ -95,21 +93,25 @@ func Test_Federation(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) roomId := "test-room" federatedRoomId := roomId + "@federated" - room1 := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, room1.Room.RoomId) - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) room := hub1.getRoom(roomId) require.NotNil(room) now := time.Now() - userdata := api.StringMap{ + userdata := map[string]interface{}{ "displayname": "Federated user", "actorType": "federated_users", "actorId": "the-federated-user-id", @@ -117,13 +119,13 @@ func Test_Federation(t *testing.T) { token, err := client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"2", now, now.Add(time.Minute), userdata) require.NoError(err) - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "join-room-fed", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, @@ -133,16 +135,16 @@ func Test_Federation(t *testing.T) { } require.NoError(client2.WriteJSON(msg)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId, message.Room.RoomId) } // The client1 will see the remote session id for client2. - var remoteSessionId api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -152,7 +154,7 @@ func Test_Federation(t *testing.T) { } // The client2 will see its own session id, not the one from the remote server. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) tmpRoom1 := hub2.getRoom(roomId) assert.Nil(tmpRoom1) @@ -170,24 +172,23 @@ func Test_Federation(t *testing.T) { request1 := getPingRequests(t) clearPingRequests(t) - if assert.Len(request1, 1) { - if ping := request1[0].Ping; assert.NotNil(ping) { - assert.Equal(roomId, ping.RoomId) - assert.Equal("1.0", ping.Version) - assert.Len(ping.Entries, 2) - // The order of entries is not defined - if ping.Entries[0].SessionId == api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)) { - assert.Equal(hello2.Hello.UserId, ping.Entries[0].UserId) + assert.Len(request1, 1) + if ping := request1[0].Ping; assert.NotNil(ping) { + assert.Equal(roomId, ping.RoomId) + assert.Equal("1.0", ping.Version) + assert.Len(ping.Entries, 2) + // The order of entries is not defined + if ping.Entries[0].SessionId == federatedRoomId+"-"+hello2.Hello.SessionId { + assert.Equal(hello2.Hello.UserId, ping.Entries[0].UserId) - assert.EqualValues(fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), ping.Entries[1].SessionId) - assert.Equal(hello1.Hello.UserId, ping.Entries[1].UserId) - } else { - assert.EqualValues(fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), ping.Entries[0].SessionId) - assert.Equal(hello1.Hello.UserId, ping.Entries[0].UserId) + assert.Equal(roomId+"-"+hello1.Hello.SessionId, ping.Entries[1].SessionId) + assert.Equal(hello1.Hello.UserId, ping.Entries[1].UserId) + } else { + assert.Equal(roomId+"-"+hello1.Hello.SessionId, ping.Entries[0].SessionId) + assert.Equal(hello1.Hello.UserId, ping.Entries[0].UserId) - assert.EqualValues(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId), ping.Entries[1].SessionId) - assert.Equal(hello2.Hello.UserId, ping.Entries[1].UserId) - } + assert.Equal(federatedRoomId+"-"+hello2.Hello.SessionId, ping.Entries[1].SessionId) + assert.Equal(hello2.Hello.UserId, ping.Entries[1].UserId) } } @@ -198,42 +199,41 @@ func Test_Federation(t *testing.T) { request2 := getPingRequests(t) clearPingRequests(t) - if assert.Len(request2, 1) { - if ping := request2[0].Ping; assert.NotNil(ping) { - assert.Equal(federatedRoomId, ping.RoomId) - assert.Equal("1.0", ping.Version) - assert.Len(ping.Entries, 1) - assert.EqualValues(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId), ping.Entries[0].SessionId) - assert.Equal(hello2.Hello.UserId, ping.Entries[0].UserId) - } + assert.Len(request2, 1) + if ping := request2[0].Ping; assert.NotNil(ping) { + assert.Equal(federatedRoomId, ping.RoomId) + assert.Equal("1.0", ping.Version) + assert.Len(ping.Entries, 1) + assert.Equal(federatedRoomId+"-"+hello2.Hello.SessionId, ping.Entries[0].SessionId) + assert.Equal(hello2.Hello.UserId, ping.Entries[0].UserId) } // Leaving and re-joining a room as "direct" session will trigger correct events. - if room, ok := client1.JoinRoom(ctx, ""); ok { - assert.Empty(room.Room.RoomId) + if room, err := client1.JoinRoom(ctx, ""); assert.NoError(err) { + assert.Equal("", room.Room.RoomId) } - client2.RunUntilLeft(ctx, hello1.Hello) + assert.NoError(client2.RunUntilLeft(ctx, hello1.Hello)) - if room, ok := client1.JoinRoom(ctx, roomId); ok { + if room, err := client1.JoinRoom(ctx, roomId); assert.NoError(err) { assert.Equal(roomId, room.Room.RoomId) } - client1.RunUntilJoined(ctx, hello1.Hello, &api.HelloServerMessage{ + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello, &HelloServerMessage{ SessionId: remoteSessionId, UserId: hello2.Hello.UserId, - }) - client2.RunUntilJoined(ctx, hello1.Hello) + })) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello)) // Leaving and re-joining a room as "federated" session will trigger correct events. - if room, ok := client2.JoinRoom(ctx, ""); ok { - assert.Empty(room.Room.RoomId) + if room, err := client2.JoinRoom(ctx, ""); assert.NoError(err) { + assert.Equal("", room.Room.RoomId) } - client1.RunUntilLeft(ctx, &api.HelloServerMessage{ + assert.NoError(client1.RunUntilLeft(ctx, &HelloServerMessage{ SessionId: remoteSessionId, UserId: hello2.Hello.UserId, - }) + })) // The federated session has left the room, so no more pings. count3, wg3 := hub2.publishFederatedSessions() @@ -241,117 +241,123 @@ func Test_Federation(t *testing.T) { assert.Equal(0, count3) require.NoError(client2.WriteJSON(msg)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId, message.Room.RoomId) } // Client1 will receive the updated "remoteSessionId" - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) - if assert.Len(message.Event.Join, 1, "invalid message received: %+v", message) { - evt := message.Event.Join[0] - remoteSessionId = evt.SessionId - assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) - assert.Equal(testDefaultUserId+"2", evt.UserId) - assert.True(evt.Federated) - assert.Equal(features2, evt.Features) - } + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) + evt := message.Event.Join[0] + remoteSessionId = evt.SessionId + assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) + assert.Equal(testDefaultUserId+"2", evt.UserId) + assert.True(evt.Federated) + assert.Equal(features2, evt.Features) } - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) // Test sending messages between sessions. data1 := "from-1-to-2" data2 := "from-2-to-1" - if assert.NoError(client1.SendMessage(api.MessageClientMessageRecipient{ + if assert.NoError(client1.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: remoteSessionId, }, data1)) { var payload string - if checkReceiveClientMessage(ctx, t, client2, "session", hello1.Hello, &payload) { + if assert.NoError(checkReceiveClientMessage(ctx, client2, "session", hello1.Hello, &payload)) { assert.Equal(data1, payload) } } - if assert.NoError(client1.SendControl(api.MessageClientMessageRecipient{ + if assert.NoError(client1.SendControl(MessageClientMessageRecipient{ Type: "session", SessionId: remoteSessionId, }, data1)) { var payload string - if checkReceiveClientControl(ctx, t, client2, "session", hello1.Hello, &payload) { + if assert.NoError(checkReceiveClientControl(ctx, client2, "session", hello1.Hello, &payload)) { assert.Equal(data1, payload) } } - if assert.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + if assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, }, data2)) { var payload string - if checkReceiveClientMessage(ctx, t, client1, "session", &api.HelloServerMessage{ + if assert.NoError(checkReceiveClientMessage(ctx, client1, "session", &HelloServerMessage{ SessionId: remoteSessionId, UserId: testDefaultUserId + "2", - }, &payload) { + }, &payload)) { assert.Equal(data2, payload) } } - if assert.NoError(client2.SendControl(api.MessageClientMessageRecipient{ + if assert.NoError(client2.SendControl(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, }, data2)) { var payload string - if checkReceiveClientControl(ctx, t, client1, "session", &api.HelloServerMessage{ + if assert.NoError(checkReceiveClientControl(ctx, client1, "session", &HelloServerMessage{ SessionId: remoteSessionId, UserId: testDefaultUserId + "2", - }, &payload) { + }, &payload)) { assert.Equal(data2, payload) } } // Special handling for the "forceMute" control event. - forceMute := api.StringMap{ + forceMute := map[string]any{ "action": "forceMute", "peerId": remoteSessionId, } - if assert.NoError(client1.SendControl(api.MessageClientMessageRecipient{ + if assert.NoError(client1.SendControl(MessageClientMessageRecipient{ Type: "session", SessionId: remoteSessionId, }, forceMute)) { - var payload api.StringMap - if checkReceiveClientControl(ctx, t, client2, "session", hello1.Hello, &payload) { + var payload map[string]any + if assert.NoError(checkReceiveClientControl(ctx, client2, "session", hello1.Hello, &payload)) { // The sessionId in "peerId" will be replaced with the local one. - forceMute["peerId"] = string(hello2.Hello.SessionId) + forceMute["peerId"] = hello2.Hello.SessionId assert.Equal(forceMute, payload) } } data3 := "from-2-to-2" // Clients can't send to their own (local) session id. - if assert.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + if assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, }, data3)) { ctx2, cancel2 := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx2); err == nil { + assert.Fail("expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } } // Clients can't send to their own (remote) session id. - if assert.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + if assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: remoteSessionId, }, data3)) { ctx2, cancel2 := context.WithTimeout(ctx, 200*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx2); err == nil { + assert.Fail("expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } } // Simulate request from the backend that a federated user joined the call. - users := []api.StringMap{ + users := []map[string]interface{}{ { "sessionId": remoteSessionId, "inCall": 1, @@ -360,24 +366,22 @@ func Test_Federation(t *testing.T) { }, } room.PublishUsersInCallChanged(users, users) - var event *api.EventServerMessage + var event *EventServerMessage // For the local user, it's a federated user on server 2 that joined. - if checkReceiveClientEvent(ctx, t, client1, "update", &event) { - assert.EqualValues(remoteSessionId, event.Update.Users[0]["sessionId"]) - assert.Equal("remoteUser@"+strings.TrimPrefix(server2.URL, "http://"), event.Update.Users[0]["actorId"]) - assert.Equal("federated_users", event.Update.Users[0]["actorType"]) - assert.Equal(roomId, event.Update.RoomId) - } + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", &event)) + assert.Equal(remoteSessionId, event.Update.Users[0]["sessionId"]) + assert.Equal("remoteUser@"+strings.TrimPrefix(server2.URL, "http://"), event.Update.Users[0]["actorId"]) + assert.Equal("federated_users", event.Update.Users[0]["actorType"]) + assert.Equal(roomId, event.Update.RoomId) // For the federated user, it's a local user that joined. - if checkReceiveClientEvent(ctx, t, client2, "update", &event) { - assert.EqualValues(hello2.Hello.SessionId, event.Update.Users[0]["sessionId"]) - assert.Equal("remoteUser", event.Update.Users[0]["actorId"]) - assert.Equal("users", event.Update.Users[0]["actorType"]) - assert.Equal(federatedRoomId, event.Update.RoomId) - } + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", &event)) + assert.Equal(hello2.Hello.SessionId, event.Update.Users[0]["sessionId"]) + assert.Equal("remoteUser", event.Update.Users[0]["actorId"]) + assert.Equal("users", event.Update.Users[0]["actorType"]) + assert.Equal(federatedRoomId, event.Update.RoomId) // Simulate request from the backend that a local user joined the call. - users = []api.StringMap{ + users = []map[string]interface{}{ { "sessionId": hello1.Hello.SessionId, "inCall": 1, @@ -387,19 +391,17 @@ func Test_Federation(t *testing.T) { } room.PublishUsersInCallChanged(users, users) // For the local user, it's a local user that joined. - if checkReceiveClientEvent(ctx, t, client1, "update", &event) { - assert.EqualValues(hello1.Hello.SessionId, event.Update.Users[0]["sessionId"]) - assert.Equal("localUser", event.Update.Users[0]["actorId"]) - assert.Equal("users", event.Update.Users[0]["actorType"]) - assert.Equal(roomId, event.Update.RoomId) - } + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", &event)) + assert.Equal(hello1.Hello.SessionId, event.Update.Users[0]["sessionId"]) + assert.Equal("localUser", event.Update.Users[0]["actorId"]) + assert.Equal("users", event.Update.Users[0]["actorType"]) + assert.Equal(roomId, event.Update.RoomId) // For the federated user, it's a federated user on server 1 that joined. - if checkReceiveClientEvent(ctx, t, client2, "update", &event) { - assert.EqualValues(hello1.Hello.SessionId, event.Update.Users[0]["sessionId"]) - assert.Equal("localUser@"+strings.TrimPrefix(server1.URL, "http://"), event.Update.Users[0]["actorId"]) - assert.Equal("federated_users", event.Update.Users[0]["actorType"]) - assert.Equal(federatedRoomId, event.Update.RoomId) - } + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", &event)) + assert.Equal(hello1.Hello.SessionId, event.Update.Users[0]["sessionId"]) + assert.Equal("localUser@"+strings.TrimPrefix(server1.URL, "http://"), event.Update.Users[0]["actorId"]) + assert.Equal("federated_users", event.Update.Users[0]["actorType"]) + assert.Equal(federatedRoomId, event.Update.RoomId) // Joining another "direct" session will trigger correct events. @@ -407,19 +409,20 @@ func Test_Federation(t *testing.T) { defer client3.CloseWithBye() require.NoError(client3.SendHelloV2(testDefaultUserId + "3")) - hello3 := MustSucceed1(t, client3.RunUntilHello, ctx) + hello3, err := client3.RunUntilHello(ctx) + require.NoError(err) - if room, ok := client3.JoinRoom(ctx, roomId); ok { + if room, err := client3.JoinRoom(ctx, roomId); assert.NoError(err) { require.Equal(roomId, room.Room.RoomId) } - client1.RunUntilJoined(ctx, hello3.Hello) - client2.RunUntilJoined(ctx, hello3.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello3.Hello)) + assert.NoError(client2.RunUntilJoined(ctx, hello3.Hello)) - client3.RunUntilJoined(ctx, hello1.Hello, &api.HelloServerMessage{ + assert.NoError(client3.RunUntilJoined(ctx, hello1.Hello, &HelloServerMessage{ SessionId: remoteSessionId, UserId: hello2.Hello.UserId, - }, hello3.Hello) + }, hello3.Hello)) // Joining another "federated" session will trigger correct events. @@ -427,9 +430,10 @@ func Test_Federation(t *testing.T) { defer client4.CloseWithBye() require.NoError(client4.SendHelloV2WithFeatures(testDefaultUserId+"4", features2)) - hello4 := MustSucceed1(t, client4.RunUntilHello, ctx) + hello4, err := client4.RunUntilHello(ctx) + require.NoError(err) - userdata = api.StringMap{ + userdata = map[string]interface{}{ "displayname": "Federated user 2", "actorType": "federated_users", "actorId": "the-other-federated-user-id", @@ -437,13 +441,13 @@ func Test_Federation(t *testing.T) { token, err = client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"4", now, now.Add(time.Minute), userdata) require.NoError(err) - msg = &api.ClientMessage{ + msg = &ClientMessage{ Id: "join-room-fed", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello4.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + SessionId: federatedRoomId + "-" + hello4.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, @@ -453,16 +457,16 @@ func Test_Federation(t *testing.T) { } require.NoError(client4.WriteJSON(msg)) - if message, ok := client4.RunUntilMessage(ctx); ok { + if message, err := client4.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId, message.Room.RoomId) } // The client1 will see the remote session id for client4. - var remoteSessionId4 api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) + var remoteSessionId4 string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId4 = evt.SessionId assert.NotEqual(hello4.Hello.SessionId, remoteSessionId) @@ -471,28 +475,30 @@ func Test_Federation(t *testing.T) { assert.Equal(features2, evt.Features) } - client2.RunUntilJoined(ctx, &api.HelloServerMessage{ + assert.NoError(client2.RunUntilJoined(ctx, &HelloServerMessage{ SessionId: remoteSessionId4, UserId: hello4.Hello.UserId, - }) + })) - client3.RunUntilJoined(ctx, &api.HelloServerMessage{ + assert.NoError(client3.RunUntilJoined(ctx, &HelloServerMessage{ SessionId: remoteSessionId4, UserId: hello4.Hello.UserId, - }) + })) - client4.RunUntilJoined(ctx, hello1.Hello, &api.HelloServerMessage{ + assert.NoError(client4.RunUntilJoined(ctx, hello1.Hello, &HelloServerMessage{ SessionId: remoteSessionId, UserId: hello2.Hello.UserId, - }, hello3.Hello, hello4.Hello) + }, hello3.Hello, hello4.Hello)) - if room3, ok := client2.JoinRoom(ctx, ""); ok { - assert.Empty(room3.Room.RoomId) + room3, err := client2.JoinRoom(ctx, "") + if assert.NoError(err) { + assert.Equal("", room3.Room.RoomId) } } func Test_FederationJoinRoomTwice(t *testing.T) { - t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) require := require.New(t) @@ -509,18 +515,22 @@ func Test_FederationJoinRoomTwice(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) roomId := "test-room" federatedRoomId := roomId + "@federated" - room1 := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, room1.Room.RoomId) - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) now := time.Now() - userdata := api.StringMap{ + userdata := map[string]interface{}{ "displayname": "Federated user", "actorType": "federated_users", "actorId": "the-federated-user-id", @@ -528,13 +538,13 @@ func Test_FederationJoinRoomTwice(t *testing.T) { token, err := client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"2", now, now.Add(time.Minute), userdata) require.NoError(err) - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "join-room-fed", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, @@ -544,16 +554,16 @@ func Test_FederationJoinRoomTwice(t *testing.T) { } require.NoError(client2.WriteJSON(msg)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId, message.Room.RoomId) } // The client1 will see the remote session id for client2. - var remoteSessionId api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -562,15 +572,15 @@ func Test_FederationJoinRoomTwice(t *testing.T) { } // The client2 will see its own session id, not the one from the remote server. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) - msg2 := &api.ClientMessage{ + msg2 := &ClientMessage{ Id: "join-room-fed-2", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, @@ -580,13 +590,13 @@ func Test_FederationJoinRoomTwice(t *testing.T) { } require.NoError(client2.WriteJSON(msg2)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg2.Id, message.Id) if assert.Equal("error", message.Type) { assert.Equal("already_joined", message.Error.Code) } if assert.NotNil(message.Error.Details) { - var roomMsg api.RoomErrorDetails + var roomMsg RoomErrorDetails if assert.NoError(json.Unmarshal(message.Error.Details, &roomMsg)) { if assert.NotNil(roomMsg.Room) { assert.Equal(federatedRoomId, roomMsg.Room.RoomId) @@ -598,7 +608,8 @@ func Test_FederationJoinRoomTwice(t *testing.T) { } func Test_FederationChangeRoom(t *testing.T) { - t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) require := require.New(t) @@ -615,18 +626,22 @@ func Test_FederationChangeRoom(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) roomId := "test-room" federatedRoomId := roomId + "@federated" - room1 := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, room1.Room.RoomId) - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) now := time.Now() - userdata := api.StringMap{ + userdata := map[string]interface{}{ "displayname": "Federated user", "actorType": "federated_users", "actorId": "the-federated-user-id", @@ -634,13 +649,13 @@ func Test_FederationChangeRoom(t *testing.T) { token, err := client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"2", now, now.Add(time.Minute), userdata) require.NoError(err) - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "join-room-fed", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, @@ -650,7 +665,7 @@ func Test_FederationChangeRoom(t *testing.T) { } require.NoError(client2.WriteJSON(msg)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId, message.Room.RoomId) @@ -659,12 +674,12 @@ func Test_FederationChangeRoom(t *testing.T) { session2 := hub2.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) fed := session2.GetFederationClient() require.NotNil(fed) - localAddr := fed.LocalAddr() + localAddr := fed.conn.LocalAddr() // The client1 will see the remote session id for client2. - var remoteSessionId api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -673,17 +688,17 @@ func Test_FederationChangeRoom(t *testing.T) { } // The client2 will see its own session id, not the one from the remote server. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) roomId2 := roomId + "-2" federatedRoomId2 := roomId2 + "@federated" - msg2 := &api.ClientMessage{ + msg2 := &ClientMessage{ Id: "join-room-fed-2", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: federatedRoomId2, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId2, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + SessionId: federatedRoomId2 + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId2, @@ -693,7 +708,7 @@ func Test_FederationChangeRoom(t *testing.T) { } require.NoError(client2.WriteJSON(msg2)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg2.Id, message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId2, message.Room.RoomId) @@ -701,12 +716,13 @@ func Test_FederationChangeRoom(t *testing.T) { fed2 := session2.GetFederationClient() require.NotNil(fed2) - localAddr2 := fed2.LocalAddr() + localAddr2 := fed2.conn.LocalAddr() assert.Equal(localAddr, localAddr2) } func Test_FederationMedia(t *testing.T) { - t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) require := require.New(t) @@ -715,13 +731,15 @@ func Test_FederationMedia(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - mcu1 := test.NewSFU(t) + mcu1, err := NewTestMCU() + require.NoError(err) require.NoError(mcu1.Start(ctx)) defer mcu1.Stop() hub1.SetMcu(mcu1) - mcu2 := test.NewSFU(t) + mcu2, err := NewTestMCU() + require.NoError(err) require.NoError(mcu2.Start(ctx)) defer mcu2.Stop() @@ -735,18 +753,22 @@ func Test_FederationMedia(t *testing.T) { defer client2.CloseWithBye() require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) roomId := "test-room" - federatedRoomId := roomId + "@federated" - room1 := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + federatedRooId := roomId + "@federated" + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, room1.Room.RoomId) - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) now := time.Now() - userdata := api.StringMap{ + userdata := map[string]interface{}{ "displayname": "Federated user", "actorType": "federated_users", "actorId": "the-federated-user-id", @@ -754,13 +776,13 @@ func Test_FederationMedia(t *testing.T) { token, err := client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"2", now, now.Add(time.Minute), userdata) require.NoError(err) - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "join-room-fed", Type: "room", - Room: &api.RoomClientMessage{ - RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + Room: &RoomClientMessage{ + RoomId: federatedRooId, + SessionId: federatedRooId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, @@ -770,16 +792,16 @@ func Test_FederationMedia(t *testing.T) { } require.NoError(client2.WriteJSON(msg)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) - require.Equal(federatedRoomId, message.Room.RoomId) + require.Equal(federatedRooId, message.Room.RoomId) } // The client1 will see the remote session id for client2. - var remoteSessionId api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -788,29 +810,30 @@ func Test_FederationMedia(t *testing.T) { } // The client2 will see its own session id, not the one from the remote server. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) - require.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "offer", Sid: "12345", RoomType: "screen", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, }, })) - client2.RunUntilAnswerFromSender(ctx, mock.MockSdpAnswerAudioAndVideo, &api.MessageServerMessageSender{ + require.NoError(client2.RunUntilAnswerFromSender(ctx, MockSdpAnswerAudioAndVideo, &MessageServerMessageSender{ Type: "session", SessionId: hello2.Hello.SessionId, UserId: hello2.Hello.UserId, - }) + })) } func Test_FederationResume(t *testing.T) { - t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) require := require.New(t) @@ -827,18 +850,22 @@ func Test_FederationResume(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) roomId := "test-room" federatedRoomId := roomId + "@federated" - room1 := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, room1.Room.RoomId) - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) now := time.Now() - userdata := api.StringMap{ + userdata := map[string]interface{}{ "displayname": "Federated user", "actorType": "federated_users", "actorId": "the-federated-user-id", @@ -846,13 +873,13 @@ func Test_FederationResume(t *testing.T) { token, err := client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"2", now, now.Add(time.Minute), userdata) require.NoError(err) - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "join-room-fed", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, @@ -862,16 +889,16 @@ func Test_FederationResume(t *testing.T) { } require.NoError(client2.WriteJSON(msg)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId, message.Room.RoomId) } // The client1 will see the remote session id for client2. - var remoteSessionId api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -880,7 +907,7 @@ func Test_FederationResume(t *testing.T) { } // The client2 will see its own session id, not the one from the remote server. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) session2 := hub2.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) fed2 := session2.GetFederationClient() @@ -889,20 +916,20 @@ func Test_FederationResume(t *testing.T) { err = fed2.conn.Close() data2 := "from-2-to-1" - assert.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + assert.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, }, data2)) fed2.mu.Unlock() assert.NoError(err) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal("event", message.Type) assert.Equal("room", message.Event.Target) assert.Equal("federation_interrupted", message.Event.Type) } - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal("event", message.Type) assert.Equal("room", message.Event.Target) assert.Equal("federation_resumed", message.Event.Type) @@ -914,23 +941,32 @@ func Test_FederationResume(t *testing.T) { defer cancel1() var payload string - if checkReceiveClientMessage(ctx, t, client1, "session", &api.HelloServerMessage{ + if assert.NoError(checkReceiveClientMessage(ctx, client1, "session", &HelloServerMessage{ SessionId: remoteSessionId, UserId: testDefaultUserId + "2", - }, &payload) { + }, &payload)) { assert.Equal(data2, payload) } - client1.RunUntilErrorIs(ctx1, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client1.RunUntilMessage(ctx1); err != nil && err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } else { + assert.Nil(message) + } ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx2); err != nil && err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } else { + assert.Nil(message) + } } func Test_FederationResumeNewSession(t *testing.T) { - t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) require := require.New(t) @@ -947,18 +983,22 @@ func Test_FederationResumeNewSession(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) roomId := "test-room" federatedRoomId := roomId + "@federated" - room1 := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + room1, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, room1.Room.RoomId) - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) now := time.Now() - userdata := api.StringMap{ + userdata := map[string]interface{}{ "displayname": "Federated user", "actorType": "federated_users", "actorId": "the-federated-user-id", @@ -966,13 +1006,13 @@ func Test_FederationResumeNewSession(t *testing.T) { token, err := client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"2", now, now.Add(time.Minute), userdata) require.NoError(err) - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "join-room-fed", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ + SessionId: federatedRoomId + "-" + hello2.Hello.SessionId, + Federation: &RoomFederationMessage{ SignalingUrl: server1.URL, NextcloudUrl: server1.URL, RoomId: roomId, @@ -982,16 +1022,16 @@ func Test_FederationResumeNewSession(t *testing.T) { } require.NoError(client2.WriteJSON(msg)) - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal(msg.Id, message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId, message.Room.RoomId) } // The client1 will see the remote session id for client2. - var remoteSessionId api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) + var remoteSessionId string + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] remoteSessionId = evt.SessionId assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -1000,7 +1040,7 @@ func Test_FederationResumeNewSession(t *testing.T) { } // The client2 will see its own session id, not the one from the remote server. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) remoteSession2 := hub1.GetSessionByPublicId(remoteSessionId).(*ClientSession) // Simulate disconnected federated client with an expired session. @@ -1010,13 +1050,13 @@ func Test_FederationResumeNewSession(t *testing.T) { } remoteSession2.Close() - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal("event", message.Type) assert.Equal("room", message.Event.Target) assert.Equal("federation_interrupted", message.Event.Type) } - if message, ok := client2.RunUntilMessage(ctx); ok { + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal("event", message.Type) assert.Equal("room", message.Event.Target) assert.Equal("federation_resumed", message.Event.Type) @@ -1026,12 +1066,12 @@ func Test_FederationResumeNewSession(t *testing.T) { // Client1 will get a "leave" for the expired session and a "join" with the // new remote session id. - client1.RunUntilLeft(ctx, &api.HelloServerMessage{ + assert.NoError(client1.RunUntilLeft(ctx, &HelloServerMessage{ SessionId: remoteSessionId, UserId: hello2.Hello.UserId, - }) - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) + })) + if message, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkSingleMessageJoined(message)) evt := message.Event.Join[0] assert.NotEqual(remoteSessionId, evt.SessionId) assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) @@ -1044,120 +1084,10 @@ func Test_FederationResumeNewSession(t *testing.T) { // client2 will join the room again after the reconnect with the new // session and get "joined" events for all sessions in the room (including // its own). - if message, ok := client2.RunUntilMessage(ctx); ok { - assert.Empty(message.Id) + if message, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("", message.Id) require.Equal("room", message.Type) require.Equal(federatedRoomId, message.Room.RoomId) } - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) -} - -func Test_FederationTransientData(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - - hub1, hub2, server1, server2 := CreateClusteredHubsForTest(t) - - client1 := NewTestClient(t, server1, hub1) - defer client1.CloseWithBye() - require.NoError(client1.SendHelloV2(testDefaultUserId + "1")) - - client2 := NewTestClient(t, server2, hub2) - defer client2.CloseWithBye() - require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) - - roomId := "test-room" - federatedRoomId := roomId + "@federated" - room1 := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, room1.Room.RoomId) - - client1.RunUntilJoined(ctx, hello1.Hello) - - now := time.Now() - userdata := api.StringMap{ - "displayname": "Federated user", - "actorType": "federated_users", - "actorId": "the-federated-user-id", - } - token, err := client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"2", now, now.Add(time.Minute), userdata) - require.NoError(err) - - msg := &api.ClientMessage{ - Id: "join-room-fed", - Type: "room", - Room: &api.RoomClientMessage{ - RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ - SignalingUrl: server1.URL, - NextcloudUrl: server1.URL, - RoomId: roomId, - Token: token, - }, - }, - } - require.NoError(client2.WriteJSON(msg)) - - if message, ok := client2.RunUntilMessage(ctx); ok { - assert.Equal(msg.Id, message.Id) - require.Equal("room", message.Type) - require.Equal(federatedRoomId, message.Room.RoomId) - } - - // The client1 will see the remote session id for client2. - var remoteSessionId api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) - evt := message.Event.Join[0] - remoteSessionId = evt.SessionId - assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) - assert.Equal(hello2.Hello.UserId, evt.UserId) - assert.True(evt.Federated) - } - - // The client2 will see its own session id, not the one from the remote server. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) - - // Regular transient data. - require.NoError(client1.SetTransientData("foo", "bar", 0)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "foo", "bar", nil) - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "foo", "bar", nil) - } - - require.NoError(client2.SetTransientData("bar", "baz", 0)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "bar", "baz", nil) - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "bar", "baz", nil) - } - - // Transient session data - sessionKey1 := "sd:" + string(hello1.Hello.SessionId) - require.NoError(client1.SetTransientData(sessionKey1, "12345", 0)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey1, "12345", nil) - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey1, "12345", nil) - } - - sessionKey2 := "sd:" + string(hello2.Hello.SessionId) - require.NoError(client2.SetTransientData(sessionKey2, "54321", 0)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "sd:"+string(remoteSessionId), "54321", nil) - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey2, "54321", nil) - } + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) } diff --git a/security/internal/file_watcher.go b/file_watcher.go similarity index 64% rename from security/internal/file_watcher.go rename to file_watcher.go index e1d48db..6d3a923 100644 --- a/security/internal/file_watcher.go +++ b/file_watcher.go @@ -19,47 +19,63 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( "context" "errors" + "log" "os" "path" "path/filepath" "strings" "sync" + "sync/atomic" "time" "github.com/fsnotify/fsnotify" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) const ( - DefaultDeduplicateWatchEvents = 100 * time.Millisecond + defaultDeduplicateWatchEvents = 100 * time.Millisecond ) +var ( + deduplicateWatchEvents atomic.Int64 +) + +func init() { + deduplicateWatchEvents.Store(int64(defaultDeduplicateWatchEvents)) +} + type FileWatcherCallback func(filename string) type FileWatcher struct { - logger log.Logger - filename string - target string - callback FileWatcherCallback - deduplicate time.Duration + filename string + target string + callback FileWatcherCallback watcher *fsnotify.Watcher closeCtx context.Context closeFunc context.CancelFunc } -func NewFileWatcher(logger log.Logger, filename string, callback FileWatcherCallback, deduplicate time.Duration) (*FileWatcher, error) { +func NewFileWatcher(filename string, callback FileWatcherCallback) (*FileWatcher, error) { + realFilename, err := filepath.EvalSymlinks(filename) + if err != nil { + return nil, err + } + watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } + if err := watcher.Add(realFilename); err != nil { + watcher.Close() // nolint + return nil, err + } + if err := watcher.Add(path.Dir(filename)); err != nil { watcher.Close() // nolint return nil, err @@ -68,38 +84,18 @@ func NewFileWatcher(logger log.Logger, filename string, callback FileWatcherCall closeCtx, closeFunc := context.WithCancel(context.Background()) w := &FileWatcher{ - logger: logger, - filename: filename, - callback: callback, - deduplicate: deduplicate, - watcher: watcher, + filename: filename, + target: realFilename, + callback: callback, + watcher: watcher, closeCtx: closeCtx, closeFunc: closeFunc, } - if err := w.updateWatcher(); err != nil { - watcher.Close() // nolint - return nil, err - } - go w.run() return w, nil } -func (f *FileWatcher) updateWatcher() error { - realFilename, err := filepath.EvalSymlinks(f.filename) - if err != nil { - return err - } - - if err := f.watcher.Add(realFilename); err != nil { - return err - } - - f.target = realFilename - return nil -} - func (f *FileWatcher) Close() error { f.closeFunc() return f.watcher.Close() @@ -110,56 +106,42 @@ func (f *FileWatcher) run() { timers := make(map[string]*time.Timer) triggerEvent := func(event fsnotify.Event) { - if f.deduplicate <= 0 { + deduplicate := time.Duration(deduplicateWatchEvents.Load()) + if deduplicate <= 0 { f.callback(f.filename) return } - filename := path.Clean(event.Name) - // Use timer to deduplicate multiple events for the same file. mu.Lock() - t, found := timers[filename] + t, found := timers[event.Name] mu.Unlock() if !found { - t = time.AfterFunc(f.deduplicate, func() { + t = time.AfterFunc(deduplicate, func() { f.callback(f.filename) mu.Lock() - delete(timers, filename) + delete(timers, event.Name) mu.Unlock() }) mu.Lock() - timers[filename] = t + timers[event.Name] = t mu.Unlock() } else { - t.Reset(f.deduplicate) + t.Reset(deduplicate) } } for { select { case event := <-f.watcher.Events: - if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) && !event.Has(fsnotify.Rename) && !event.Has(fsnotify.Remove) { - continue - } - - if event.Has(fsnotify.Remove) { - // Watched target has been deleted, assume it was symlinked and try to watch new target. - if event.Name != f.target { - continue - } - - triggerEvent(event) - if err := f.updateWatcher(); err != nil { - f.logger.Printf("Error updating watcher after %s is deleted: %s", event.Name, err) - } + if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) && !event.Has(fsnotify.Rename) { continue } if stat, err := os.Lstat(event.Name); err != nil { if !errors.Is(err, os.ErrNotExist) { - f.logger.Printf("Could not lstat %s: %s", event.Name, err) + log.Printf("Could not lstat %s: %s", event.Name, err) } } else if stat.Mode()&os.ModeSymlink != 0 { target, err := filepath.EvalSymlinks(event.Name) @@ -178,7 +160,7 @@ func (f *FileWatcher) run() { return } - f.logger.Printf("Error watching %s: %s", f.filename, err) + log.Printf("Error watching %s: %s", f.filename, err) case <-f.closeCtx.Done(): return } diff --git a/security/internal/file_watcher_test.go b/file_watcher_test.go similarity index 54% rename from security/internal/file_watcher_test.go rename to file_watcher_test.go index 7e515c7..14dec15 100644 --- a/security/internal/file_watcher_test.go +++ b/file_watcher_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( "context" @@ -29,83 +29,34 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" ) var ( - testWatcherNoEventTimeout = 2 * DefaultDeduplicateWatchEvents + testWatcherNoEventTimeout = 2 * defaultDeduplicateWatchEvents ) func TestFileWatcher_NotExist(t *testing.T) { - t.Parallel() assert := assert.New(t) tmpdir := t.TempDir() - logger := logtest.NewLoggerForTest(t) - if w, err := NewFileWatcher(logger, path.Join(tmpdir, "test.txt"), func(filename string) {}, DefaultDeduplicateWatchEvents); !assert.ErrorIs(err, os.ErrNotExist) { + if w, err := NewFileWatcher(path.Join(tmpdir, "test.txt"), func(filename string) {}); !assert.ErrorIs(err, os.ErrNotExist) { if w != nil { assert.NoError(w.Close()) } } } -func TestFileWatcher_File(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { +func TestFileWatcher_File(t *testing.T) { + ensureNoGoroutinesLeak(t, func(t *testing.T) { require := require.New(t) assert := assert.New(t) tmpdir := t.TempDir() filename := path.Join(tmpdir, "test.txt") require.NoError(os.WriteFile(filename, []byte("Hello world!"), 0644)) - logger := logtest.NewLoggerForTest(t) modified := make(chan struct{}) - w, err := NewFileWatcher(logger, filename, func(filename string) { + w, err := NewFileWatcher(filename, func(filename string) { modified <- struct{}{} - }, DefaultDeduplicateWatchEvents) - require.NoError(err) - defer w.Close() - - require.NoError(os.WriteFile(filename, []byte("Updated"), 0644)) - <-modified - - ctxTimeout, cancel := context.WithTimeout(context.Background(), testWatcherNoEventTimeout) - defer cancel() - - select { - case <-modified: - assert.Fail("should not have received another event") - case <-ctxTimeout.Done(): - } - - require.NoError(os.WriteFile(filename, []byte("Updated"), 0644)) - <-modified - - ctxTimeout, cancel = context.WithTimeout(context.Background(), testWatcherNoEventTimeout) - defer cancel() - - select { - case <-modified: - assert.Fail("should not have received another event") - case <-ctxTimeout.Done(): - } - }) -} - -func TestFileWatcher_CurrentDir(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - tmpdir := t.TempDir() - t.Chdir(tmpdir) - filename := path.Join(tmpdir, "test.txt") - require.NoError(os.WriteFile(filename, []byte("Hello world!"), 0644)) - - logger := logtest.NewLoggerForTest(t) - modified := make(chan struct{}) - w, err := NewFileWatcher(logger, "./"+path.Base(filename), func(filename string) { - modified <- struct{}{} - }, DefaultDeduplicateWatchEvents) + }) require.NoError(err) defer w.Close() @@ -136,18 +87,16 @@ func TestFileWatcher_CurrentDir(t *testing.T) { // nolint:paralleltest } func TestFileWatcher_Rename(t *testing.T) { - t.Parallel() require := require.New(t) assert := assert.New(t) tmpdir := t.TempDir() filename := path.Join(tmpdir, "test.txt") require.NoError(os.WriteFile(filename, []byte("Hello world!"), 0644)) - logger := logtest.NewLoggerForTest(t) modified := make(chan struct{}) - w, err := NewFileWatcher(logger, filename, func(filename string) { + w, err := NewFileWatcher(filename, func(filename string) { modified <- struct{}{} - }, DefaultDeduplicateWatchEvents) + }) require.NoError(err) defer w.Close() @@ -177,7 +126,6 @@ func TestFileWatcher_Rename(t *testing.T) { } func TestFileWatcher_Symlink(t *testing.T) { - t.Parallel() require := require.New(t) assert := assert.New(t) tmpdir := t.TempDir() @@ -187,11 +135,10 @@ func TestFileWatcher_Symlink(t *testing.T) { filename := path.Join(tmpdir, "symlink.txt") require.NoError(os.Symlink(sourceFilename, filename)) - logger := logtest.NewLoggerForTest(t) modified := make(chan struct{}) - w, err := NewFileWatcher(logger, filename, func(filename string) { + w, err := NewFileWatcher(filename, func(filename string) { modified <- struct{}{} - }, DefaultDeduplicateWatchEvents) + }) require.NoError(err) defer w.Close() @@ -209,7 +156,6 @@ func TestFileWatcher_Symlink(t *testing.T) { } func TestFileWatcher_ChangeSymlinkTarget(t *testing.T) { - t.Parallel() require := require.New(t) assert := assert.New(t) tmpdir := t.TempDir() @@ -222,11 +168,10 @@ func TestFileWatcher_ChangeSymlinkTarget(t *testing.T) { filename := path.Join(tmpdir, "symlink.txt") require.NoError(os.Symlink(sourceFilename1, filename)) - logger := logtest.NewLoggerForTest(t) modified := make(chan struct{}) - w, err := NewFileWatcher(logger, filename, func(filename string) { + w, err := NewFileWatcher(filename, func(filename string) { modified <- struct{}{} - }, DefaultDeduplicateWatchEvents) + }) require.NoError(err) defer w.Close() @@ -246,7 +191,6 @@ func TestFileWatcher_ChangeSymlinkTarget(t *testing.T) { } func TestFileWatcher_OtherSymlink(t *testing.T) { - t.Parallel() require := require.New(t) assert := assert.New(t) tmpdir := t.TempDir() @@ -259,11 +203,10 @@ func TestFileWatcher_OtherSymlink(t *testing.T) { filename := path.Join(tmpdir, "symlink.txt") require.NoError(os.Symlink(sourceFilename1, filename)) - logger := logtest.NewLoggerForTest(t) modified := make(chan struct{}) - w, err := NewFileWatcher(logger, filename, func(filename string) { + w, err := NewFileWatcher(filename, func(filename string) { modified <- struct{}{} - }, DefaultDeduplicateWatchEvents) + }) require.NoError(err) defer w.Close() @@ -280,7 +223,6 @@ func TestFileWatcher_OtherSymlink(t *testing.T) { } func TestFileWatcher_RenameSymlinkTarget(t *testing.T) { - t.Parallel() require := require.New(t) assert := assert.New(t) tmpdir := t.TempDir() @@ -290,11 +232,10 @@ func TestFileWatcher_RenameSymlinkTarget(t *testing.T) { filename := path.Join(tmpdir, "test.txt") require.NoError(os.Symlink(sourceFilename1, filename)) - logger := logtest.NewLoggerForTest(t) modified := make(chan struct{}) - w, err := NewFileWatcher(logger, filename, func(filename string) { + w, err := NewFileWatcher(filename, func(filename string) { modified <- struct{}{} - }, DefaultDeduplicateWatchEvents) + }) require.NoError(err) defer w.Close() @@ -322,92 +263,3 @@ func TestFileWatcher_RenameSymlinkTarget(t *testing.T) { case <-ctxTimeout.Done(): } } - -func TestFileWatcher_UpdateSymlinkFolder(t *testing.T) { - t.Parallel() - // This mimics what k8s is doing with configmaps / secrets. - require := require.New(t) - assert := assert.New(t) - tmpdir := t.TempDir() - - // File is in a versioned folder. - version1Path := path.Join(tmpdir, "version1") - require.NoError(os.Mkdir(version1Path, 0755)) - sourceFilename1 := path.Join(version1Path, "test.txt") - require.NoError(os.WriteFile(sourceFilename1, []byte("Hello world!"), 0644)) - - // Versioned folder is symlinked to a generic "data" folder. - dataPath := path.Join(tmpdir, "data") - require.NoError(os.Symlink("version1", dataPath)) - - // File in root is symlinked from generic "data" folder. - filename := path.Join(tmpdir, "test.txt") - require.NoError(os.Symlink("data/test.txt", filename)) - - logger := logtest.NewLoggerForTest(t) - modified := make(chan struct{}) - w, err := NewFileWatcher(logger, filename, func(filename string) { - modified <- struct{}{} - }, DefaultDeduplicateWatchEvents) - require.NoError(err) - defer w.Close() - - // New file is created in a new versioned subfolder. - version2Path := path.Join(tmpdir, "version2") - require.NoError(os.Mkdir(version2Path, 0755)) - - sourceFilename2 := path.Join(version2Path, "test.txt") - require.NoError(os.WriteFile(sourceFilename2, []byte("Updated"), 0644)) - - ctxTimeout, cancel := context.WithTimeout(context.Background(), testWatcherNoEventTimeout) - defer cancel() - - select { - case <-modified: - assert.Fail("should not have received another event") - case <-ctxTimeout.Done(): - } - - // Create temporary symlink to new versioned subfolder... - require.NoError(os.Symlink("version2", dataPath+".tmp")) - // ...atomically update generic "data" symlink... - require.NoError(os.Rename(dataPath+".tmp", dataPath)) - // ...and old versioned subfolder is removed (this will trigger the event). - require.NoError(os.RemoveAll(version1Path)) - - <-modified - - // Another new file is created in a new versioned subfolder. - version3Path := path.Join(tmpdir, "version3") - require.NoError(os.Mkdir(version3Path, 0755)) - - sourceFilename3 := path.Join(version3Path, "test.txt") - require.NoError(os.WriteFile(sourceFilename3, []byte("Updated again"), 0644)) - - ctxTimeout, cancel = context.WithTimeout(context.Background(), testWatcherNoEventTimeout) - defer cancel() - - select { - case <-modified: - assert.Fail("should not have received another event") - case <-ctxTimeout.Done(): - } - - // Create temporary symlink to new versioned subfolder... - require.NoError(os.Symlink("version3", dataPath+".tmp")) - // ...atomically update generic "data" symlink... - require.NoError(os.Rename(dataPath+".tmp", dataPath)) - // ...and old versioned subfolder is removed (this will trigger the event). - require.NoError(os.RemoveAll(version2Path)) - - <-modified - - ctxTimeout, cancel = context.WithTimeout(context.Background(), testWatcherNoEventTimeout) - defer cancel() - - select { - case <-modified: - assert.Fail("should not have received another event") - case <-ctxTimeout.Done(): - } -} diff --git a/internal/flags.go b/flags.go similarity index 62% rename from internal/flags.go rename to flags.go index 289bdea..3f67283 100644 --- a/internal/flags.go +++ b/flags.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( "sync/atomic" @@ -30,17 +30,46 @@ type Flags struct { } func (f *Flags) Add(flags uint32) bool { - old := f.flags.Or(flags) - return old&flags != flags + for { + old := f.flags.Load() + if old&flags == flags { + // Flags already set. + return false + } + newFlags := old | flags + if f.flags.CompareAndSwap(old, newFlags) { + return true + } + // Another thread updated the flags while we were checking, retry. + } } func (f *Flags) Remove(flags uint32) bool { - old := f.flags.And(^flags) - return old&flags != 0 + for { + old := f.flags.Load() + if old&flags == 0 { + // Flags not set. + return false + } + newFlags := old & ^flags + if f.flags.CompareAndSwap(old, newFlags) { + return true + } + // Another thread updated the flags while we were checking, retry. + } } func (f *Flags) Set(flags uint32) bool { - return f.flags.Swap(flags) != flags + for { + old := f.flags.Load() + if old == flags { + return false + } + + if f.flags.CompareAndSwap(old, flags) { + return true + } + } } func (f *Flags) Get() uint32 { diff --git a/internal/flags_test.go b/flags_test.go similarity index 94% rename from internal/flags_test.go rename to flags_test.go index 84eacaf..1665167 100644 --- a/internal/flags_test.go +++ b/flags_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( "sync" @@ -30,7 +30,6 @@ import ( ) func TestFlags(t *testing.T) { - t.Parallel() assert := assert.New(t) var f Flags assert.EqualValues(0, f.Get()) @@ -55,13 +54,15 @@ func runConcurrentFlags(t *testing.T, count int, f func()) { start.Add(1) var ready sync.WaitGroup var done sync.WaitGroup - for range count { + for i := 0; i < count; i++ { + done.Add(1) ready.Add(1) - done.Go(func() { + go func() { + defer done.Done() ready.Done() start.Wait() f() - }) + }() } ready.Wait() start.Done() @@ -78,7 +79,7 @@ func TestFlagsConcurrentAdd(t *testing.T) { added.Add(1) } }) - assert.EqualValues(t, 1, added.Load(), "expected only one successful attempt") + assert.EqualValues(t, 1, added.Load(), "expected only one successfull attempt") } func TestFlagsConcurrentRemove(t *testing.T) { @@ -92,7 +93,7 @@ func TestFlagsConcurrentRemove(t *testing.T) { removed.Add(1) } }) - assert.EqualValues(t, 1, removed.Load(), "expected only one successful attempt") + assert.EqualValues(t, 1, removed.Load(), "expected only one successfull attempt") } func TestFlagsConcurrentSet(t *testing.T) { @@ -105,5 +106,5 @@ func TestFlagsConcurrentSet(t *testing.T) { set.Add(1) } }) - assert.EqualValues(t, 1, set.Load(), "expected only one successful attempt") + assert.EqualValues(t, 1, set.Load(), "expected only one successfull attempt") } diff --git a/geoip.go b/geoip.go new file mode 100644 index 0000000..ec461e5 --- /dev/null +++ b/geoip.go @@ -0,0 +1,339 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2019 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "log" + "net" + "net/http" + "net/url" + "os" + "strings" + "sync" + "time" + + "github.com/dlintw/goconf" + "github.com/oschwald/maxminddb-golang" +) + +var ( + ErrDatabaseNotInitialized = fmt.Errorf("GeoIP database not initialized yet") +) + +func GetGeoIpDownloadUrl(license string) string { + if license == "" { + return "" + } + + result := "https://download.maxmind.com/app/geoip_download" + result += "?edition_id=GeoLite2-Country" + result += "&license_key=" + url.QueryEscape(license) + result += "&suffix=tar.gz" + return result +} + +type GeoLookup struct { + url string + isFile bool + client http.Client + mu sync.Mutex + + lastModifiedHeader string + lastModifiedTime time.Time + + reader *maxminddb.Reader +} + +func NewGeoLookupFromUrl(url string) (*GeoLookup, error) { + geoip := &GeoLookup{ + url: url, + } + return geoip, nil +} + +func NewGeoLookupFromFile(filename string) (*GeoLookup, error) { + geoip := &GeoLookup{ + url: filename, + isFile: true, + } + if err := geoip.Update(); err != nil { + geoip.Close() + return nil, err + } + return geoip, nil +} + +func (g *GeoLookup) Close() { + g.mu.Lock() + if g.reader != nil { + g.reader.Close() + g.reader = nil + } + g.mu.Unlock() +} + +func (g *GeoLookup) Update() error { + if g.isFile { + return g.updateFile() + } + + return g.updateUrl() +} + +func (g *GeoLookup) updateFile() error { + info, err := os.Stat(g.url) + if err != nil { + return err + } + + if info.ModTime().Equal(g.lastModifiedTime) { + return nil + } + + reader, err := maxminddb.Open(g.url) + if err != nil { + return err + } + + if err := reader.Verify(); err != nil { + return err + } + + metadata := reader.Metadata + log.Printf("Using %s GeoIP database from %s (built on %s)", metadata.DatabaseType, g.url, time.Unix(int64(metadata.BuildEpoch), 0).UTC()) + + g.mu.Lock() + if g.reader != nil { + g.reader.Close() + } + g.reader = reader + g.lastModifiedTime = info.ModTime() + g.mu.Unlock() + return nil +} + +func (g *GeoLookup) updateUrl() error { + request, err := http.NewRequest("GET", g.url, nil) + if err != nil { + return err + } + if g.lastModifiedHeader != "" { + request.Header.Add("If-Modified-Since", g.lastModifiedHeader) + } + response, err := g.client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode == http.StatusNotModified { + log.Printf("GeoIP database at %s has not changed", g.url) + return nil + } else if response.StatusCode/100 != 2 { + return fmt.Errorf("downloading %s returned an error: %s", g.url, response.Status) + } + + body := response.Body + url := g.url + if strings.HasSuffix(url, ".gz") { + body, err = gzip.NewReader(body) + if err != nil { + return err + } + url = strings.TrimSuffix(url, ".gz") + } + + var geoipdata []byte + if strings.HasSuffix(url, ".tar") || strings.HasSuffix(url, "=tar") { + tarfile := tar.NewReader(body) + for { + header, err := tarfile.Next() + if err == io.EOF { + break + } else if err != nil { + return err + } + + if !strings.HasSuffix(header.Name, ".mmdb") { + continue + } + + geoipdata, err = io.ReadAll(tarfile) + if err != nil { + return err + } + break + } + } else { + geoipdata, err = io.ReadAll(body) + if err != nil { + return err + } + } + + if len(geoipdata) == 0 { + return fmt.Errorf("did not find GeoIP database in download from %s", g.url) + } + + reader, err := maxminddb.FromBytes(geoipdata) + if err != nil { + return err + } + + if err := reader.Verify(); err != nil { + return err + } + + metadata := reader.Metadata + log.Printf("Using %s GeoIP database from %s (built on %s)", metadata.DatabaseType, g.url, time.Unix(int64(metadata.BuildEpoch), 0).UTC()) + + g.mu.Lock() + if g.reader != nil { + g.reader.Close() + } + g.reader = reader + g.lastModifiedHeader = response.Header.Get("Last-Modified") + g.mu.Unlock() + return nil +} + +func (g *GeoLookup) LookupCountry(ip net.IP) (string, error) { + var record struct { + Country struct { + ISOCode string `maxminddb:"iso_code"` + } `maxminddb:"country"` + } + + g.mu.Lock() + if g.reader == nil { + g.mu.Unlock() + return "", ErrDatabaseNotInitialized + } + err := g.reader.Lookup(ip, &record) + g.mu.Unlock() + if err != nil { + return "", err + } + + return record.Country.ISOCode, nil +} + +func LookupContinents(country string) []string { + continents, found := ContinentMap[country] + if !found { + return nil + } + + return continents +} + +func IsValidContinent(continent string) bool { + switch continent { + case "AF": + // Africa + fallthrough + case "AN": + // Antartica + fallthrough + case "AS": + // Asia + fallthrough + case "EU": + // Europe + fallthrough + case "NA": + // North America + fallthrough + case "SA": + // South America + fallthrough + case "OC": + // Oceania + return true + default: + return false + } +} + +func LoadGeoIPOverrides(config *goconf.ConfigFile, ignoreErrors bool) (map[*net.IPNet]string, error) { + options, _ := GetStringOptions(config, "geoip-overrides", true) + if len(options) == 0 { + return nil, nil + } + + var err error + geoipOverrides := make(map[*net.IPNet]string, len(options)) + for option, value := range options { + var ip net.IP + var ipNet *net.IPNet + if strings.Contains(option, "/") { + _, ipNet, err = net.ParseCIDR(option) + if err != nil { + if ignoreErrors { + log.Printf("could not parse CIDR %s (%s), skipping", option, err) + continue + } + + return nil, fmt.Errorf("could not parse CIDR %s: %s", option, err) + } + } else { + ip = net.ParseIP(option) + if ip == nil { + if ignoreErrors { + log.Printf("could not parse IP %s, skipping", option) + continue + } + + return nil, fmt.Errorf("could not parse IP %s", option) + } + + var mask net.IPMask + if ipv4 := ip.To4(); ipv4 != nil { + mask = net.CIDRMask(32, 32) + } else { + mask = net.CIDRMask(128, 128) + } + ipNet = &net.IPNet{ + IP: ip, + Mask: mask, + } + } + + value = strings.ToUpper(strings.TrimSpace(value)) + if value == "" { + log.Printf("IP %s doesn't have a country assigned, skipping", option) + continue + } else if !IsValidCountry(value) { + log.Printf("Country %s for IP %s is invalid, skipping", value, option) + continue + } + + log.Printf("Using country %s for %s", value, ipNet) + geoipOverrides[ipNet] = value + } + + return geoipOverrides, nil +} diff --git a/geoip/geoip.go b/geoip/geoip.go deleted file mode 100644 index a020a38..0000000 --- a/geoip/geoip.go +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package geoip - -import "strings" - -type ( - Country string - Continent string -) - -var ( - NoCountry = Country("no-country") - noCountryUpper = Country(strings.ToUpper("no-country")) - - Loopback = Country("loopback") - loopbackUpper = Country(strings.ToUpper("loopback")) - - UnknownCountry = Country("unknown-country") - unknownCountryUpper = Country(strings.ToUpper("unknown-country")) -) - -func IsValidCountry(country Country) bool { - switch country { - case "": - fallthrough - case NoCountry: - fallthrough - case noCountryUpper: - fallthrough - case Loopback: - fallthrough - case loopbackUpper: - fallthrough - case UnknownCountry: - fallthrough - case unknownCountryUpper: - return false - default: - return true - } -} - -func LookupContinents(country Country) []Continent { - continents, found := ContinentMap[country] - if !found { - return nil - } - - return continents -} - -func IsValidContinent(continent Continent) bool { - switch continent { - case "AF": - // Africa - fallthrough - case "AN": - // Antartica - fallthrough - case "AS": - // Asia - fallthrough - case "EU": - // Europe - fallthrough - case "NA": - // North America - fallthrough - case "SA": - // South America - fallthrough - case "OC": - // Oceania - return true - default: - return false - } -} diff --git a/geoip/maxmind.go b/geoip/maxmind.go deleted file mode 100644 index 52fdd02..0000000 --- a/geoip/maxmind.go +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2019 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package geoip - -import ( - "archive/tar" - "compress/gzip" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "os" - "strings" - "sync/atomic" - "time" - - "github.com/oschwald/maxminddb-golang" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" -) - -var ( - ErrDatabaseNotInitialized = errors.New("GeoIP database not initialized yet") -) - -func GetMaxMindDownloadUrl(license string) string { - if license == "" { - return "" - } - - result := "https://download.maxmind.com/app/geoip_download" - result += "?edition_id=GeoLite2-Country" - result += "&license_key=" + url.QueryEscape(license) - result += "&suffix=tar.gz" - return result -} - -type Lookup struct { - logger log.Logger - url string - isFile bool - client http.Client - - lastModifiedHeader atomic.Value - lastModifiedTime atomic.Int64 - - reader atomic.Pointer[maxminddb.Reader] -} - -func NewLookupFromUrl(logger log.Logger, url string) (*Lookup, error) { - geoip := &Lookup{ - logger: logger, - url: url, - } - return geoip, nil -} - -func NewLookupFromFile(logger log.Logger, filename string) (*Lookup, error) { - geoip := &Lookup{ - logger: logger, - url: filename, - isFile: true, - } - if err := geoip.Update(); err != nil { - geoip.Close() - return nil, err - } - return geoip, nil -} - -func (g *Lookup) Close() { - if reader := g.reader.Swap(nil); reader != nil { - reader.Close() - } -} - -func (g *Lookup) Update() error { - if g.isFile { - return g.updateFile() - } - - return g.updateUrl() -} - -func (g *Lookup) updateFile() error { - info, err := os.Stat(g.url) - if err != nil { - return err - } - - if info.ModTime().UnixNano() == g.lastModifiedTime.Load() { - return nil - } - - reader, err := maxminddb.Open(g.url) - if err != nil { - return err - } - - if err := reader.Verify(); err != nil { - return err - } - - metadata := reader.Metadata - g.logger.Printf("Using %s GeoIP database from %s (built on %s)", metadata.DatabaseType, g.url, time.Unix(int64(metadata.BuildEpoch), 0).UTC()) - - if old := g.reader.Swap(reader); old != nil { - old.Close() - } - - g.lastModifiedTime.Store(info.ModTime().UnixNano()) - return nil -} - -func (g *Lookup) updateUrl() error { - request, err := http.NewRequest("GET", g.url, nil) - if err != nil { - return err - } - if header := g.lastModifiedHeader.Load(); header != nil { - request.Header.Add("If-Modified-Since", header.(string)) - } - response, err := g.client.Do(request) - if err != nil { - return err - } - defer response.Body.Close() - - if response.StatusCode == http.StatusNotModified { - g.logger.Printf("GeoIP database at %s has not changed", g.url) - return nil - } else if response.StatusCode/100 != 2 { - return fmt.Errorf("downloading %s returned an error: %s", g.url, response.Status) - } - - body := response.Body - url := g.url - if strings.HasSuffix(url, ".gz") { - body, err = gzip.NewReader(body) - if err != nil { - return err - } - url = strings.TrimSuffix(url, ".gz") - } - - var geoipdata []byte - if strings.HasSuffix(url, ".tar") || strings.HasSuffix(url, "=tar") { - tarfile := tar.NewReader(body) - for { - header, err := tarfile.Next() - if err == io.EOF { - break - } else if err != nil { - return err - } - - if !strings.HasSuffix(header.Name, ".mmdb") { - continue - } - - geoipdata, err = io.ReadAll(tarfile) - if err != nil { - return err - } - break - } - } else { - geoipdata, err = io.ReadAll(body) - if err != nil { - return err - } - } - - if len(geoipdata) == 0 { - return fmt.Errorf("did not find GeoIP database in download from %s", g.url) - } - - reader, err := maxminddb.FromBytes(geoipdata) - if err != nil { - return err - } - - if err := reader.Verify(); err != nil { - return err - } - - metadata := reader.Metadata - g.logger.Printf("Using %s GeoIP database from %s (built on %s)", metadata.DatabaseType, g.url, time.Unix(int64(metadata.BuildEpoch), 0).UTC()) - - if old := g.reader.Swap(reader); old != nil { - old.Close() - } - - g.lastModifiedHeader.Store(response.Header.Get("Last-Modified")) - return nil -} - -func (g *Lookup) LookupCountry(ip net.IP) (Country, error) { - var record struct { - Country struct { - ISOCode string `maxminddb:"iso_code"` - } `maxminddb:"country"` - } - - reader := g.reader.Load() - if reader == nil { - return "", ErrDatabaseNotInitialized - } - - if err := reader.Lookup(ip, &record); err != nil { - return "", err - } - - return Country(record.Country.ISOCode), nil -} diff --git a/geoip/overrides.go b/geoip/overrides.go deleted file mode 100644 index fd522f2..0000000 --- a/geoip/overrides.go +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package geoip - -import ( - "context" - "fmt" - "maps" - "net" - "strings" - "sync/atomic" - - "github.com/dlintw/goconf" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" -) - -type Overrides map[*net.IPNet]Country - -func (o Overrides) Lookup(ip net.IP) (Country, bool) { - for overrideNet, country := range o { - if overrideNet.Contains(ip) { - return country, true - } - } - - return UnknownCountry, false -} - -type AtomicOverrides struct { - value atomic.Pointer[Overrides] -} - -func (a *AtomicOverrides) Store(value Overrides) { - if len(value) == 0 { - a.value.Store(nil) - } else { - v := maps.Clone(value) - a.value.Store(&v) - } -} - -func (a *AtomicOverrides) Load() Overrides { - value := a.value.Load() - if value == nil { - return nil - } - - return *value -} - -func LoadOverrides(ctx context.Context, cfg *goconf.ConfigFile, ignoreErrors bool) (Overrides, error) { - logger := log.LoggerFromContext(ctx) - options, _ := config.GetStringOptions(cfg, "geoip-overrides", true) - if len(options) == 0 { - return nil, nil - } - - var err error - geoipOverrides := make(Overrides, len(options)) - for option, value := range options { - var ip net.IP - var ipNet *net.IPNet - if strings.Contains(option, "/") { - _, ipNet, err = net.ParseCIDR(option) - if err != nil { - if ignoreErrors { - logger.Printf("could not parse CIDR %s (%s), skipping", option, err) - continue - } - - return nil, fmt.Errorf("could not parse CIDR %s: %w", option, err) - } - } else { - ip = net.ParseIP(option) - if ip == nil { - if ignoreErrors { - logger.Printf("could not parse IP %s, skipping", option) - continue - } - - return nil, fmt.Errorf("could not parse IP %s", option) - } - - var mask net.IPMask - if ipv4 := ip.To4(); ipv4 != nil { - mask = net.CIDRMask(32, 32) - } else { - mask = net.CIDRMask(128, 128) - } - ipNet = &net.IPNet{ - IP: ip, - Mask: mask, - } - } - - value = strings.ToUpper(strings.TrimSpace(value)) - if value == "" { - logger.Printf("IP %s doesn't have a country assigned, skipping", option) - continue - } else if !IsValidCountry(Country(value)) { - logger.Printf("Country %s for IP %s is invalid, skipping", value, option) - continue - } - - logger.Printf("Using country %s for %s", value, ipNet) - geoipOverrides[ipNet] = Country(value) - } - - return geoipOverrides, nil -} diff --git a/geoip/overrides_test.go b/geoip/overrides_test.go deleted file mode 100644 index 211eef4..0000000 --- a/geoip/overrides_test.go +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package geoip - -import ( - "maps" - "net" - "testing" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" -) - -func mustSucceed1[T any, A1 any](t *testing.T, f func(a1 A1) (T, bool), a1 A1) T { - t.Helper() - result, ok := f(a1) - if !ok { - t.FailNow() - } - return result -} - -func TestOverridesEmpty(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - config := goconf.NewConfigFile() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - overrides, err := LoadOverrides(ctx, config, true) - require.NoError(err) - assert.Empty(overrides) -} - -func TestOverrides(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - config := goconf.NewConfigFile() - config.AddOption("geoip-overrides", "10.1.0.0/16", "DE") - config.AddOption("geoip-overrides", "2001:db8::/48", "FR") - config.AddOption("geoip-overrides", "2001:db9::3", "CH") - config.AddOption("geoip-overrides", "10.3.4.5", "custom") - config.AddOption("geoip-overrides", "10.4.5.6", "loopback") - config.AddOption("geoip-overrides", "192.168.1.0", "") - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - overrides, err := LoadOverrides(ctx, config, true) - require.NoError(err) - - if assert.Len(overrides, 4) { - assert.EqualValues("DE", mustSucceed1(t, overrides.Lookup, net.ParseIP("10.1.2.3"))) - assert.EqualValues("DE", mustSucceed1(t, overrides.Lookup, net.ParseIP("10.1.3.4"))) - assert.EqualValues("FR", mustSucceed1(t, overrides.Lookup, net.ParseIP("2001:db8::1"))) - assert.EqualValues("FR", mustSucceed1(t, overrides.Lookup, net.ParseIP("2001:db8::2"))) - assert.EqualValues("CH", mustSucceed1(t, overrides.Lookup, net.ParseIP("2001:db9::3"))) - assert.EqualValues("CUSTOM", mustSucceed1(t, overrides.Lookup, net.ParseIP("10.3.4.5"))) - - country, ok := overrides.Lookup(net.ParseIP("10.4.5.6")) - assert.False(ok, "expected no country, got %s", country) - - country, ok = overrides.Lookup(net.ParseIP("192.168.1.0")) - assert.False(ok, "expected no country, got %s", country) - } -} - -func TestOverridesInvalidIgnoreErrors(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - config := goconf.NewConfigFile() - config.AddOption("geoip-overrides", "invalid-ip", "DE") - config.AddOption("geoip-overrides", "300.1.2.3/8", "DE") - config.AddOption("geoip-overrides", "10.2.0.0/16", "FR") - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - overrides, err := LoadOverrides(ctx, config, true) - require.NoError(err) - - if assert.Len(overrides, 1) { - assert.EqualValues("FR", mustSucceed1(t, overrides.Lookup, net.ParseIP("10.2.3.4"))) - assert.EqualValues("FR", mustSucceed1(t, overrides.Lookup, net.ParseIP("10.2.4.5"))) - - country, ok := overrides.Lookup(net.ParseIP("10.3.4.5")) - assert.False(ok, "expected no country, got %s", country) - - country, ok = overrides.Lookup(net.ParseIP("192.168.1.0")) - assert.False(ok, "expected no country, got %s", country) - } -} - -func TestOverridesInvalidIPReturnErrors(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - config := goconf.NewConfigFile() - config.AddOption("geoip-overrides", "invalid-ip", "DE") - config.AddOption("geoip-overrides", "10.2.0.0/16", "FR") - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - overrides, err := LoadOverrides(ctx, config, false) - assert.ErrorContains(err, "could not parse IP", err) - assert.Empty(overrides) -} - -func TestOverridesInvalidCIDRReturnErrors(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - config := goconf.NewConfigFile() - config.AddOption("geoip-overrides", "300.1.2.3/8", "DE") - config.AddOption("geoip-overrides", "10.2.0.0/16", "FR") - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - - overrides, err := LoadOverrides(ctx, config, false) - var e *net.ParseError - assert.ErrorAs(err, &e) - assert.Empty(overrides) -} - -func TestAtomicOverrides(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - overrides := make(Overrides) - overrides[&net.IPNet{ - IP: net.ParseIP("10.1.2.3."), - Mask: net.CIDRMask(32, 32), - }] = "DE" - - var value AtomicOverrides - assert.Nil(value.Load()) - value.Store(make(Overrides)) - assert.Nil(value.Load()) - value.Store(overrides) - if o := value.Load(); assert.NotEmpty(o) { - assert.Equal(overrides, o) - } - // Updating the overrides doesn't change the stored value. - overrides2 := maps.Clone(overrides) - overrides[&net.IPNet{ - IP: net.ParseIP("10.1.2.3."), - Mask: net.CIDRMask(32, 32), - }] = "FR" - if o := value.Load(); assert.NotEmpty(o) { - assert.Equal(overrides2, o) - } -} diff --git a/geoip/maxmind_test.go b/geoip_test.go similarity index 75% rename from geoip/maxmind_test.go rename to geoip_test.go index ba6b139..4d1a1e1 100644 --- a/geoip/maxmind_test.go +++ b/geoip_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package geoip +package signaling import ( "archive/tar" @@ -35,12 +35,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" ) -func testLookupReader(t *testing.T, reader *Lookup) { - tests := map[string]Country{ +func testGeoLookupReader(t *testing.T, reader *GeoLookup) { + tests := map[string]string{ // Example from maxminddb-golang code. "81.2.69.142": "GB", // Local addresses don't have a country assigned. @@ -48,6 +46,8 @@ func testLookupReader(t *testing.T, reader *Lookup) { } for ip, expected := range tests { + ip := ip + expected := expected t.Run(ip, func(t *testing.T) { country, err := reader.LookupCountry(net.ParseIP(ip)) if !assert.NoError(t, err, "Could not lookup %s", ip) { @@ -59,7 +59,7 @@ func testLookupReader(t *testing.T, reader *Lookup) { } } -func GetIpUrlForTest(t *testing.T) string { +func GetGeoIpUrlForTest(t *testing.T) string { t.Helper() var geoIpUrl string @@ -72,29 +72,27 @@ func GetIpUrlForTest(t *testing.T) string { if license == "" { t.Skip("No MaxMind GeoLite2 license was set in MAXMIND_GEOLITE2_LICENSE environment variable.") } - geoIpUrl = GetMaxMindDownloadUrl(license) + geoIpUrl = GetGeoIpDownloadUrl(license) } return geoIpUrl } -func TestLookup(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) +func TestGeoLookup(t *testing.T) { + CatchLogForTest(t) require := require.New(t) - reader, err := NewLookupFromUrl(logger, GetIpUrlForTest(t)) + reader, err := NewGeoLookupFromUrl(GetGeoIpUrlForTest(t)) require.NoError(err) defer reader.Close() require.NoError(reader.Update()) - testLookupReader(t, reader) + testGeoLookupReader(t, reader) } -func TestLookupCaching(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) +func TestGeoLookupCaching(t *testing.T) { + CatchLogForTest(t) require := require.New(t) - reader, err := NewLookupFromUrl(logger, GetIpUrlForTest(t)) + reader, err := NewGeoLookupFromUrl(GetGeoIpUrlForTest(t)) require.NoError(err) defer reader.Close() @@ -105,21 +103,21 @@ func TestLookupCaching(t *testing.T) { require.NoError(reader.Update()) } -func TestLookupContinent(t *testing.T) { - t.Parallel() - tests := map[Country][]Continent{ - "AU": {"OC"}, - "DE": {"EU"}, - "RU": {"EU"}, - "": nil, - "INVALID": nil, +func TestGeoLookupContinent(t *testing.T) { + tests := map[string][]string{ + "AU": {"OC"}, + "DE": {"EU"}, + "RU": {"EU"}, + "": nil, + "INVALID ": nil, } for country, expected := range tests { - t.Run(string(country), func(t *testing.T) { - t.Parallel() + country := country + expected := expected + t.Run(country, func(t *testing.T) { continents := LookupContinents(country) - if !assert.Len(t, continents, len(expected), "Continents didn't match for %s: got %s, expected %s", country, continents, expected) { + if !assert.Equal(t, len(expected), len(continents), "Continents didn't match for %s: got %s, expected %s", country, continents, expected) { return } for idx, c := range expected { @@ -131,19 +129,17 @@ func TestLookupContinent(t *testing.T) { } } -func TestLookupCloseEmpty(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - reader, err := NewLookupFromUrl(logger, "ignore-url") +func TestGeoLookupCloseEmpty(t *testing.T) { + CatchLogForTest(t) + reader, err := NewGeoLookupFromUrl("ignore-url") require.NoError(t, err) reader.Close() } -func TestLookupFromFile(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) +func TestGeoLookupFromFile(t *testing.T) { + CatchLogForTest(t) require := require.New(t) - geoIpUrl := GetIpUrlForTest(t) + geoIpUrl := GetGeoIpUrlForTest(t) resp, err := http.Get(geoIpUrl) require.NoError(err) @@ -196,15 +192,14 @@ func TestLookupFromFile(t *testing.T) { require.True(foundDatabase, "Did not find GeoIP database in download from %s", geoIpUrl) - reader, err := NewLookupFromFile(logger, tmpfile.Name()) + reader, err := NewGeoLookupFromFile(tmpfile.Name()) require.NoError(err) defer reader.Close() - testLookupReader(t, reader) + testGeoLookupReader(t, reader) } func TestIsValidContinent(t *testing.T) { - t.Parallel() for country, continents := range ContinentMap { for _, continent := range continents { assert.True(t, IsValidContinent(continent), "Continent %s of country %s is not valid", continent, country) diff --git a/go.mod b/go.mod index a211d9a..691479f 100644 --- a/go.mod +++ b/go.mod @@ -1,102 +1,97 @@ -module github.com/strukturag/nextcloud-spreed-signaling/v2 +module github.com/strukturag/nextcloud-spreed-signaling -go 1.25.0 +go 1.22.0 require ( github.com/dlintw/goconf v0.0.0-20120228082610-dcc070983490 - github.com/fsnotify/fsnotify v1.9.0 - github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/fsnotify/fsnotify v1.8.0 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 + github.com/gorilla/securecookie v1.1.2 github.com/gorilla/websocket v1.5.3 - github.com/mailru/easyjson v0.9.1 - github.com/nats-io/nats-server/v2 v2.12.5 - github.com/nats-io/nats.go v1.49.0 + github.com/mailru/easyjson v0.9.0 + github.com/marcw/cachecontrol v0.0.0-20140722115028-30341fe9a7d5 + github.com/nats-io/nats-server/v2 v2.10.24 + github.com/nats-io/nats.go v1.38.0 github.com/notedit/janus-go v0.0.0-20200517101215-10eb8b95d1a0 github.com/oschwald/maxminddb-golang v1.13.1 - github.com/pion/ice/v4 v4.2.1 - github.com/pion/sdp/v3 v3.0.18 - github.com/pquerna/cachecontrol v0.2.0 - github.com/prometheus/client_golang v1.23.2 - github.com/stretchr/testify v1.11.1 - go.etcd.io/etcd/api/v3 v3.6.8 - go.etcd.io/etcd/client/pkg/v3 v3.6.8 - go.etcd.io/etcd/client/v3 v3.6.8 - go.etcd.io/etcd/server/v3 v3.6.8 - go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.2 - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 - google.golang.org/protobuf v1.36.11 + github.com/pion/sdp/v3 v3.0.10 + github.com/prometheus/client_golang v1.20.5 + github.com/stretchr/testify v1.10.0 + go.etcd.io/etcd/api/v3 v3.5.17 + go.etcd.io/etcd/client/pkg/v3 v3.5.17 + go.etcd.io/etcd/client/v3 v3.5.17 + go.etcd.io/etcd/server/v3 v3.5.17 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.69.4 + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 + google.golang.org/protobuf v1.36.3 ) require ( - github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/btree v1.1.3 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-tpm v0.9.8 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect + github.com/minio/highwayhash v1.0.3 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nats-io/jwt/v2 v2.8.0 // indirect - github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/jwt/v2 v2.7.3 // indirect + github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/pion/dtls/v3 v3.1.2 // indirect - github.com/pion/logging v0.2.4 // indirect - github.com/pion/mdns/v2 v2.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/stun/v3 v3.1.1 // indirect - github.com/pion/transport/v4 v4.0.1 // indirect - github.com/pion/turn/v4 v4.1.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/soheilhy/cmux v0.1.5 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect - github.com/wlynxg/anet v0.0.5 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect - go.etcd.io/bbolt v1.4.3 // indirect - go.etcd.io/etcd/pkg/v3 v3.6.8 // indirect - go.etcd.io/raft/v3 v3.6.0 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/sdk v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + go.etcd.io/bbolt v1.3.11 // indirect + go.etcd.io/etcd/client/v2 v2.305.17 // indirect + go.etcd.io/etcd/pkg/v3 v3.5.17 // indirect + go.etcd.io/etcd/raft/v3 v3.5.17 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/sdk v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.8.0 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index 4d9b63e..24b51e2 100644 --- a/go.sum +++ b/go.sum @@ -1,219 +1,281 @@ -github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= -github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlintw/goconf v0.0.0-20120228082610-dcc070983490 h1:I8/Qu5NTaiXi1TsEYmTeLDUlf7u9pEdbG+azjDvx8Vg= github.com/dlintw/goconf v0.0.0-20120228082610-dcc070983490/go.mod h1:jWlUIP63OLr0cV2FGN2IEzSFsMAe58if8rk/SAE0JRE= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= -github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= -github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= -github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/marcw/cachecontrol v0.0.0-20140722115028-30341fe9a7d5 h1:Wnc+HxXmAhN6xRzhmPJTiip9/sVZzwa6XlWksxjObCA= +github.com/marcw/cachecontrol v0.0.0-20140722115028-30341fe9a7d5/go.mod h1:e4ZZwiqLDqvzKu9TVxuGnh2kXCWeU6PxLG2hw/+no7g= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= -github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= -github.com/nats-io/nats-server/v2 v2.12.5 h1:EOHLbsLJgUHUwzkj9gBTOlubkX+dmSs0EYWMdBiHivU= -github.com/nats-io/nats-server/v2 v2.12.5/go.mod h1:JQDAKcwdXs0NRhvYO31dzsXkzCyDkOBS7SKU3Nozu14= -github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= -github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= -github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= -github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE= +github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4= +github.com/nats-io/nats-server/v2 v2.10.24 h1:KcqqQAD0ZZcG4yLxtvSFJY7CYKVYlnlWoAiVZ6i/IY4= +github.com/nats-io/nats-server/v2 v2.10.24/go.mod h1:olvKt8E5ZlnjyqBGbAXtxvSQKsPodISK5Eo/euIta4s= +github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA= +github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw= +github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= +github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/notedit/janus-go v0.0.0-20200517101215-10eb8b95d1a0 h1:EFU9iv8BMPyBo8iFMHvQleYlF5M3PY6zpAbxsngImjE= github.com/notedit/janus-go v0.0.0-20200517101215-10eb8b95d1a0/go.mod h1:BN/Txse3qz8tZOmCm2OfajB2wHVujWmX3o9nVdsI6gE= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= -github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= -github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= -github.com/pion/ice/v4 v4.2.1 h1:XPRYXaLiFq3LFDG7a7bMrmr3mFr27G/gtXN3v/TVfxY= -github.com/pion/ice/v4 v4.2.1/go.mod h1:2quLV1S5v1tAx3VvAJaH//KGitRXvo4RKlX6D3tnN+c= -github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= -github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= -github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= -github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= -github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= -github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= -github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= -github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= -github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= -github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= -github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ= -github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ= +github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= +github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k= -github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= -github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= -go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM= -go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q= -go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50= -go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw= -go.etcd.io/etcd/client/v3 v3.6.8 h1:B3G76t1UykqAOrbio7s/EPatixQDkQBevN8/mwiplrY= -go.etcd.io/etcd/client/v3 v3.6.8/go.mod h1:MVG4BpSIuumPi+ELF7wYtySETmoTWBHVcDoHdVupwt8= -go.etcd.io/etcd/pkg/v3 v3.6.8 h1:Xe+LIL974spy8b4nEx3H0KMr1ofq3r0kh6FbU3aw4es= -go.etcd.io/etcd/pkg/v3 v3.6.8/go.mod h1:TRibVNe+FqJIe1abOAA1PsuQ4wqO87ZaOoprg09Tn8c= -go.etcd.io/etcd/server/v3 v3.6.8 h1:U2strdSEy1U8qcSzRIdkYpvOPtBy/9i/IfaaCI9flZ4= -go.etcd.io/etcd/server/v3 v3.6.8/go.mod h1:88dCtwUnSirkUoJbflQxxWXqtBSZa6lSG0Kuej+dois= -go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= -go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= +go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4= +go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw= +go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w= +go.etcd.io/etcd/client/v2 v2.305.17 h1:ajFukQfI//xY5VuSeuUw4TJ4WnNR2kAFfV/P0pDdPMs= +go.etcd.io/etcd/client/v2 v2.305.17/go.mod h1:EttKgEgvwikmXN+b7pkEWxDZr6sEaYsqCiS3k4fa/Vg= +go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY= +go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo= +go.etcd.io/etcd/pkg/v3 v3.5.17 h1:1k2wZ+oDp41jrk3F9o15o8o7K3/qliBo0mXqxo1PKaE= +go.etcd.io/etcd/pkg/v3 v3.5.17/go.mod h1:FrztuSuaJG0c7RXCOzT08w+PCugh2kCQXmruNYCpCGA= +go.etcd.io/etcd/raft/v3 v3.5.17 h1:wHPW/b1oFBw/+HjDAQ9vfr17OIInejTIsmwMZpK1dNo= +go.etcd.io/etcd/raft/v3 v3.5.17/go.mod h1:uapEfOMPaJ45CqBYIraLO5+fqyIY2d57nFfxzFwy4D4= +go.etcd.io/etcd/server/v3 v3.5.17 h1:xykBwLZk9IdDsB8z8rMdCCPRvhrG+fwvARaGA0TRiyc= +go.etcd.io/etcd/server/v3 v3.5.17/go.mod h1:40sqgtGt6ZJNKm8nk8x6LexZakPu+NDl/DCgZTZ69Cc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -221,27 +283,44 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 h1:/WILD1UcXj/ujCxgoL/DvRgt2CP3txG8+FwkUbb9110= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1/go.mod h1:YNKnb2OAApgYn2oYY47Rn7alMr1zWjb2U8Q0aoGWiNc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/grpc/client_test.go b/grpc/client_test.go deleted file mode 100644 index 6f6680c..0000000 --- a/grpc/client_test.go +++ /dev/null @@ -1,285 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2022 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package grpc - -import ( - "context" - "fmt" - "net" - "testing" - "time" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" - dnstest "github.com/strukturag/nextcloud-spreed-signaling/v2/dns/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" -) - -func NewClientsForTestWithConfig(t *testing.T, config *goconf.ConfigFile, etcdClient etcd.Client, lookup *dnstest.MockLookup) (*Clients, *dns.Monitor) { - dnsMonitor := dnstest.NewMonitorForTest(t, time.Hour, lookup) // will be updated manually - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - client, err := NewClients(ctx, config, etcdClient, dnsMonitor, "0.0.0") - require.NoError(t, err) - t.Cleanup(func() { - client.Close() - }) - - return client, dnsMonitor -} - -func NewClientsForTest(t *testing.T, addr string, lookup *dnstest.MockLookup) (*Clients, *dns.Monitor) { - config := goconf.NewConfigFile() - config.AddOption("grpc", "targets", addr) - config.AddOption("grpc", "dnsdiscovery", "true") - - return NewClientsForTestWithConfig(t, config, nil, lookup) -} - -func NewClientsWithEtcdForTest(t *testing.T, embedEtcd *etcdtest.EtcdServer, lookup *dnstest.MockLookup) (*Clients, *dns.Monitor) { - config := goconf.NewConfigFile() - config.AddOption("etcd", "endpoints", embedEtcd.URL().String()) - - config.AddOption("grpc", "targettype", "etcd") - config.AddOption("grpc", "targetprefix", "/grpctargets") - - logger := logtest.NewLoggerForTest(t) - etcdClient, err := etcd.NewClient(logger, config, "") - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, etcdClient.Close()) - }) - - return NewClientsForTestWithConfig(t, config, etcdClient, lookup) -} - -func waitForEvent(ctx context.Context, t *testing.T, ch <-chan struct{}) { - t.Helper() - - select { - case <-ch: - return - case <-ctx.Done(): - assert.Fail(t, "timeout waiting for event") - } -} - -func Test_GrpcClients_DnsDiscovery(t *testing.T) { // nolint:paralleltest - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - lookup := dnstest.NewMockLookup() - target := "testgrpc:12345" - ip1 := net.ParseIP("192.168.0.1") - ip2 := net.ParseIP("192.168.0.2") - targetWithIp1 := fmt.Sprintf("%s (%s)", target, ip1) - targetWithIp2 := fmt.Sprintf("%s (%s)", target, ip2) - lookup.Set("testgrpc", []net.IP{ip1}) - client, dnsMonitor := NewClientsForTest(t, target, lookup) - ch := client.GetWakeupChannelForTesting() - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - // Wait for initial check to be done to make sure internal dnsmonitor goroutine is waiting. - if err := dnsMonitor.WaitForTicker(ctx); err != nil { - require.NoError(err) - } - - test.DrainWakeupChannel(ch) - dnsMonitor.CheckHostnames() - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(targetWithIp1, clients[0].Target()) - assert.True(clients[0].ip.Equal(ip1), "Expected IP %s, got %s", ip1, clients[0].ip) - } - - lookup.Set("testgrpc", []net.IP{ip1, ip2}) - test.DrainWakeupChannel(ch) - dnsMonitor.CheckHostnames() - waitForEvent(ctx, t, ch) - - if clients := client.GetClients(); assert.Len(clients, 2) { - assert.Equal(targetWithIp1, clients[0].Target()) - assert.True(clients[0].ip.Equal(ip1), "Expected IP %s, got %s", ip1, clients[0].ip) - assert.Equal(targetWithIp2, clients[1].Target()) - assert.True(clients[1].ip.Equal(ip2), "Expected IP %s, got %s", ip2, clients[1].ip) - } - - lookup.Set("testgrpc", []net.IP{ip2}) - test.DrainWakeupChannel(ch) - dnsMonitor.CheckHostnames() - waitForEvent(ctx, t, ch) - - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(targetWithIp2, clients[0].Target()) - assert.True(clients[0].ip.Equal(ip2), "Expected IP %s, got %s", ip2, clients[0].ip) - } - }) -} - -func Test_GrpcClients_DnsDiscoveryInitialFailed(t *testing.T) { - t.Parallel() - assert := assert.New(t) - lookup := dnstest.NewMockLookup() - target := "testgrpc:12345" - ip1 := net.ParseIP("192.168.0.1") - targetWithIp1 := fmt.Sprintf("%s (%s)", target, ip1) - client, dnsMonitor := NewClientsForTest(t, target, lookup) - ch := client.GetWakeupChannelForTesting() - - testCtx, testCtxCancel := context.WithTimeout(context.Background(), testTimeout) - defer testCtxCancel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - require.NoError(t, client.WaitForInitialized(ctx)) - - assert.Empty(client.GetClients()) - - lookup.Set("testgrpc", []net.IP{ip1}) - test.DrainWakeupChannel(ch) - dnsMonitor.CheckHostnames() - waitForEvent(testCtx, t, ch) - - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(targetWithIp1, clients[0].Target()) - assert.True(clients[0].ip.Equal(ip1), "Expected IP %s, got %s", ip1, clients[0].ip) - } -} - -func Test_GrpcClients_EtcdInitial(t *testing.T) { // nolint:paralleltest - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - _, addr1 := NewServerForTest(t) - _, addr2 := NewServerForTest(t) - - embedEtcd := etcdtest.NewServerForTest(t) - - embedEtcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) - - client, _ := NewClientsWithEtcdForTest(t, embedEtcd, nil) - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - require.NoError(t, client.WaitForInitialized(ctx)) - - clients := client.GetClients() - assert.Len(t, clients, 2, "Expected two clients, got %+v", clients) - }) -} - -func Test_GrpcClients_EtcdUpdate(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - assert := assert.New(t) - embedEtcd := etcdtest.NewServerForTest(t) - client, _ := NewClientsWithEtcdForTest(t, embedEtcd, nil) - ch := client.GetWakeupChannelForTesting() - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - assert.Empty(client.GetClients()) - - test.DrainWakeupChannel(ch) - _, addr1 := NewServerForTest(t) - embedEtcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) - waitForEvent(ctx, t, ch) - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(addr1, clients[0].Target()) - } - - test.DrainWakeupChannel(ch) - _, addr2 := NewServerForTest(t) - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) - waitForEvent(ctx, t, ch) - if clients := client.GetClients(); assert.Len(clients, 2) { - assert.Equal(addr1, clients[0].Target()) - assert.Equal(addr2, clients[1].Target()) - } - - test.DrainWakeupChannel(ch) - embedEtcd.DeleteValue("/grpctargets/one") - waitForEvent(ctx, t, ch) - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(addr2, clients[0].Target()) - } - - test.DrainWakeupChannel(ch) - _, addr3 := NewServerForTest(t) - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr3+"\"}")) - waitForEvent(ctx, t, ch) - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(addr3, clients[0].Target()) - } -} - -func Test_GrpcClients_EtcdIgnoreSelf(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - assert := assert.New(t) - embedEtcd := etcdtest.NewServerForTest(t) - client, _ := NewClientsWithEtcdForTest(t, embedEtcd, nil) - ch := client.GetWakeupChannelForTesting() - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - assert.Empty(client.GetClients()) - - test.DrainWakeupChannel(ch) - _, addr1 := NewServerForTest(t) - embedEtcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) - waitForEvent(ctx, t, ch) - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(addr1, clients[0].Target()) - } - - test.DrainWakeupChannel(ch) - server2, addr2 := NewServerForTest(t) - server2.serverId = ServerId - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) - waitForEvent(ctx, t, ch) - client.WaitForSelfCheck() - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(addr1, clients[0].Target()) - } - - test.DrainWakeupChannel(ch) - embedEtcd.DeleteValue("/grpctargets/two") - waitForEvent(ctx, t, ch) - if clients := client.GetClients(); assert.Len(clients, 1) { - assert.Equal(addr1, clients[0].Target()) - } -} diff --git a/grpc/internal.pb.go b/grpc/internal.pb.go deleted file mode 100644 index 243f459..0000000 --- a/grpc/internal.pb.go +++ /dev/null @@ -1,359 +0,0 @@ -//* -// Standalone signaling server for the Nextcloud Spreed app. -// Copyright (C) 2022 struktur AG -// -// @author Joachim Bauch -// -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: grpc/internal.proto - -package grpc - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type GetServerIdRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetServerIdRequest) Reset() { - *x = GetServerIdRequest{} - mi := &file_grpc_internal_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetServerIdRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetServerIdRequest) ProtoMessage() {} - -func (x *GetServerIdRequest) ProtoReflect() protoreflect.Message { - mi := &file_grpc_internal_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetServerIdRequest.ProtoReflect.Descriptor instead. -func (*GetServerIdRequest) Descriptor() ([]byte, []int) { - return file_grpc_internal_proto_rawDescGZIP(), []int{0} -} - -type GetServerIdReply struct { - state protoimpl.MessageState `protogen:"open.v1"` - ServerId string `protobuf:"bytes,1,opt,name=serverId,proto3" json:"serverId,omitempty"` - Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetServerIdReply) Reset() { - *x = GetServerIdReply{} - mi := &file_grpc_internal_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetServerIdReply) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetServerIdReply) ProtoMessage() {} - -func (x *GetServerIdReply) ProtoReflect() protoreflect.Message { - mi := &file_grpc_internal_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetServerIdReply.ProtoReflect.Descriptor instead. -func (*GetServerIdReply) Descriptor() ([]byte, []int) { - return file_grpc_internal_proto_rawDescGZIP(), []int{1} -} - -func (x *GetServerIdReply) GetServerId() string { - if x != nil { - return x.ServerId - } - return "" -} - -func (x *GetServerIdReply) GetVersion() string { - if x != nil { - return x.Version - } - return "" -} - -type GetTransientDataRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - RoomId string `protobuf:"bytes,1,opt,name=roomId,proto3" json:"roomId,omitempty"` - BackendUrls []string `protobuf:"bytes,2,rep,name=backendUrls,proto3" json:"backendUrls,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetTransientDataRequest) Reset() { - *x = GetTransientDataRequest{} - mi := &file_grpc_internal_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetTransientDataRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetTransientDataRequest) ProtoMessage() {} - -func (x *GetTransientDataRequest) ProtoReflect() protoreflect.Message { - mi := &file_grpc_internal_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetTransientDataRequest.ProtoReflect.Descriptor instead. -func (*GetTransientDataRequest) Descriptor() ([]byte, []int) { - return file_grpc_internal_proto_rawDescGZIP(), []int{2} -} - -func (x *GetTransientDataRequest) GetRoomId() string { - if x != nil { - return x.RoomId - } - return "" -} - -func (x *GetTransientDataRequest) GetBackendUrls() []string { - if x != nil { - return x.BackendUrls - } - return nil -} - -type GrpcTransientDataEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` - Expires int64 `protobuf:"varint,2,opt,name=expires,proto3" json:"expires,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GrpcTransientDataEntry) Reset() { - *x = GrpcTransientDataEntry{} - mi := &file_grpc_internal_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GrpcTransientDataEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GrpcTransientDataEntry) ProtoMessage() {} - -func (x *GrpcTransientDataEntry) ProtoReflect() protoreflect.Message { - mi := &file_grpc_internal_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GrpcTransientDataEntry.ProtoReflect.Descriptor instead. -func (*GrpcTransientDataEntry) Descriptor() ([]byte, []int) { - return file_grpc_internal_proto_rawDescGZIP(), []int{3} -} - -func (x *GrpcTransientDataEntry) GetValue() []byte { - if x != nil { - return x.Value - } - return nil -} - -func (x *GrpcTransientDataEntry) GetExpires() int64 { - if x != nil { - return x.Expires - } - return 0 -} - -type GetTransientDataReply struct { - state protoimpl.MessageState `protogen:"open.v1"` - Entries map[string]*GrpcTransientDataEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetTransientDataReply) Reset() { - *x = GetTransientDataReply{} - mi := &file_grpc_internal_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetTransientDataReply) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetTransientDataReply) ProtoMessage() {} - -func (x *GetTransientDataReply) ProtoReflect() protoreflect.Message { - mi := &file_grpc_internal_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetTransientDataReply.ProtoReflect.Descriptor instead. -func (*GetTransientDataReply) Descriptor() ([]byte, []int) { - return file_grpc_internal_proto_rawDescGZIP(), []int{4} -} - -func (x *GetTransientDataReply) GetEntries() map[string]*GrpcTransientDataEntry { - if x != nil { - return x.Entries - } - return nil -} - -var File_grpc_internal_proto protoreflect.FileDescriptor - -const file_grpc_internal_proto_rawDesc = "" + - "\n" + - "\x13grpc/internal.proto\x12\x04grpc\"\x14\n" + - "\x12GetServerIdRequest\"H\n" + - "\x10GetServerIdReply\x12\x1a\n" + - "\bserverId\x18\x01 \x01(\tR\bserverId\x12\x18\n" + - "\aversion\x18\x02 \x01(\tR\aversion\"S\n" + - "\x17GetTransientDataRequest\x12\x16\n" + - "\x06roomId\x18\x01 \x01(\tR\x06roomId\x12 \n" + - "\vbackendUrls\x18\x02 \x03(\tR\vbackendUrls\"H\n" + - "\x16GrpcTransientDataEntry\x12\x14\n" + - "\x05value\x18\x01 \x01(\fR\x05value\x12\x18\n" + - "\aexpires\x18\x02 \x01(\x03R\aexpires\"\xb5\x01\n" + - "\x15GetTransientDataReply\x12B\n" + - "\aentries\x18\x01 \x03(\v2(.grpc.GetTransientDataReply.EntriesEntryR\aentries\x1aX\n" + - "\fEntriesEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x122\n" + - "\x05value\x18\x02 \x01(\v2\x1c.grpc.GrpcTransientDataEntryR\x05value:\x028\x012\xa2\x01\n" + - "\vRpcInternal\x12A\n" + - "\vGetServerId\x12\x18.grpc.GetServerIdRequest\x1a\x16.grpc.GetServerIdReply\"\x00\x12P\n" + - "\x10GetTransientData\x12\x1d.grpc.GetTransientDataRequest\x1a\x1b.grpc.GetTransientDataReply\"\x00B7Z5github.com/strukturag/nextcloud-spreed-signaling/grpcb\x06proto3" - -var ( - file_grpc_internal_proto_rawDescOnce sync.Once - file_grpc_internal_proto_rawDescData []byte -) - -func file_grpc_internal_proto_rawDescGZIP() []byte { - file_grpc_internal_proto_rawDescOnce.Do(func() { - file_grpc_internal_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_grpc_internal_proto_rawDesc), len(file_grpc_internal_proto_rawDesc))) - }) - return file_grpc_internal_proto_rawDescData -} - -var file_grpc_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_grpc_internal_proto_goTypes = []any{ - (*GetServerIdRequest)(nil), // 0: grpc.GetServerIdRequest - (*GetServerIdReply)(nil), // 1: grpc.GetServerIdReply - (*GetTransientDataRequest)(nil), // 2: grpc.GetTransientDataRequest - (*GrpcTransientDataEntry)(nil), // 3: grpc.GrpcTransientDataEntry - (*GetTransientDataReply)(nil), // 4: grpc.GetTransientDataReply - nil, // 5: grpc.GetTransientDataReply.EntriesEntry -} -var file_grpc_internal_proto_depIdxs = []int32{ - 5, // 0: grpc.GetTransientDataReply.entries:type_name -> grpc.GetTransientDataReply.EntriesEntry - 3, // 1: grpc.GetTransientDataReply.EntriesEntry.value:type_name -> grpc.GrpcTransientDataEntry - 0, // 2: grpc.RpcInternal.GetServerId:input_type -> grpc.GetServerIdRequest - 2, // 3: grpc.RpcInternal.GetTransientData:input_type -> grpc.GetTransientDataRequest - 1, // 4: grpc.RpcInternal.GetServerId:output_type -> grpc.GetServerIdReply - 4, // 5: grpc.RpcInternal.GetTransientData:output_type -> grpc.GetTransientDataReply - 4, // [4:6] is the sub-list for method output_type - 2, // [2:4] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name -} - -func init() { file_grpc_internal_proto_init() } -func file_grpc_internal_proto_init() { - if File_grpc_internal_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_grpc_internal_proto_rawDesc), len(file_grpc_internal_proto_rawDesc)), - NumEnums: 0, - NumMessages: 6, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_grpc_internal_proto_goTypes, - DependencyIndexes: file_grpc_internal_proto_depIdxs, - MessageInfos: file_grpc_internal_proto_msgTypes, - }.Build() - File_grpc_internal_proto = out.File - file_grpc_internal_proto_goTypes = nil - file_grpc_internal_proto_depIdxs = nil -} diff --git a/grpc/internal.proto b/grpc/internal.proto deleted file mode 100644 index 3a72483..0000000 --- a/grpc/internal.proto +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2022 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -syntax = "proto3"; - -option go_package = "github.com/strukturag/nextcloud-spreed-signaling/grpc"; - -package grpc; - -service RpcInternal { - rpc GetServerId(GetServerIdRequest) returns (GetServerIdReply) {} - rpc GetTransientData(GetTransientDataRequest) returns (GetTransientDataReply) {} -} - -message GetServerIdRequest { -} - -message GetServerIdReply { - string serverId = 1; - string version = 2; -} - -message GetTransientDataRequest { - string roomId = 1; - repeated string backendUrls = 2; -} - -message GrpcTransientDataEntry { - bytes value = 1; - int64 expires = 2; -} - -message GetTransientDataReply { - map entries = 1; -} diff --git a/grpc/server.go b/grpc/server.go deleted file mode 100644 index 1a99f8c..0000000 --- a/grpc/server.go +++ /dev/null @@ -1,338 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2022 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package grpc - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net" - "net/url" - - "github.com/dlintw/goconf" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - status "google.golang.org/grpc/status" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -func init() { - RegisterServerStats() -} - -type ServerHub interface { - GetSessionIdByResumeId(resumeId api.PrivateSessionId) api.PublicSessionId - GetSessionIdByRoomSessionId(roomSessionId api.RoomSessionId) (api.PublicSessionId, error) - IsSessionIdInCall(sessionId api.PublicSessionId, roomId string, backendUrl string) (bool, bool) - - DisconnectSessionByRoomSessionId(sessionId api.PublicSessionId, roomSessionId api.RoomSessionId, reason string) - - GetBackend(u *url.URL) *talk.Backend - GetInternalSessions(roomId string, backend *talk.Backend) ([]*InternalSessionData, []*VirtualSessionData, bool) - GetTransientEntries(roomId string, backend *talk.Backend) (api.TransientDataEntries, bool) - GetPublisherIdForSessionId(ctx context.Context, sessionId api.PublicSessionId, streamType sfu.StreamType) (*GetPublisherIdReply, error) - - ProxySession(request RpcSessions_ProxySessionServer) error -} - -type Server struct { - UnimplementedRpcBackendServer - UnimplementedRpcInternalServer - UnimplementedRpcMcuServer - UnimplementedRpcSessionsServer - - logger log.Logger - version string - creds credentials.TransportCredentials - conn *grpc.Server - listener net.Listener - serverId string // can be overwritten from tests - - hub ServerHub -} - -func NewServer(ctx context.Context, cfg *goconf.ConfigFile, version string) (*Server, error) { - var listener net.Listener - if addr, _ := config.GetStringOptionWithEnv(cfg, "grpc", "listen"); addr != "" { - var err error - listener, err = net.Listen("tcp", addr) - if err != nil { - return nil, fmt.Errorf("could not create GRPC listener %s: %w", addr, err) - } - } - - logger := log.LoggerFromContext(ctx) - creds, err := NewReloadableCredentials(logger, cfg, true) - if err != nil { - return nil, err - } - - conn := grpc.NewServer(grpc.Creds(creds)) - result := &Server{ - logger: logger, - version: version, - creds: creds, - conn: conn, - listener: listener, - serverId: ServerId, - } - RegisterRpcBackendServer(conn, result) - RegisterRpcInternalServer(conn, result) - RegisterRpcSessionsServer(conn, result) - RegisterRpcMcuServer(conn, result) - return result, nil -} - -func (s *Server) SetHub(hub ServerHub) { - s.hub = hub -} - -func (s *Server) SetServerId(serverId string) { - s.serverId = serverId -} - -func (s *Server) Run() error { - if s.listener == nil { - return nil - } - - return s.conn.Serve(s.listener) -} - -type SimpleCloser interface { - Close() -} - -func (s *Server) Close() { - s.conn.GracefulStop() - if cr, ok := s.creds.(SimpleCloser); ok { - cr.Close() - } -} - -func (s *Server) CloseUnclean() { - s.conn.Stop() -} - -func (s *Server) LookupResumeId(ctx context.Context, request *LookupResumeIdRequest) (*LookupResumeIdReply, error) { - statsGrpcServerCalls.WithLabelValues("LookupResumeId").Inc() - // TODO: Remove debug logging - s.logger.Printf("Lookup session for resume id %s", request.ResumeId) - sessionId := s.hub.GetSessionIdByResumeId(api.PrivateSessionId(request.ResumeId)) - if sessionId == "" { - return nil, status.Error(codes.NotFound, "no such room session id") - } - - return &LookupResumeIdReply{ - SessionId: string(sessionId), - }, nil -} - -func (s *Server) LookupSessionId(ctx context.Context, request *LookupSessionIdRequest) (*LookupSessionIdReply, error) { - statsGrpcServerCalls.WithLabelValues("LookupSessionId").Inc() - // TODO: Remove debug logging - s.logger.Printf("Lookup session id for room session id %s", request.RoomSessionId) - sid, err := s.hub.GetSessionIdByRoomSessionId(api.RoomSessionId(request.RoomSessionId)) - if errors.Is(err, ErrNoSuchRoomSession) { - return nil, status.Error(codes.NotFound, "no such room session id") - } else if err != nil { - return nil, err - } - - if sid != "" && request.DisconnectReason != "" { - s.hub.DisconnectSessionByRoomSessionId(sid, api.RoomSessionId(request.RoomSessionId), request.DisconnectReason) - } - return &LookupSessionIdReply{ - SessionId: string(sid), - }, nil -} - -func (s *Server) IsSessionInCall(ctx context.Context, request *IsSessionInCallRequest) (*IsSessionInCallReply, error) { - statsGrpcServerCalls.WithLabelValues("IsSessionInCall").Inc() - // TODO: Remove debug logging - s.logger.Printf("Check if session %s is in call %s on %s", request.SessionId, request.RoomId, request.BackendUrl) - - found, inCall := s.hub.IsSessionIdInCall(api.PublicSessionId(request.SessionId), request.GetRoomId(), request.GetBackendUrl()) - if !found { - return nil, status.Error(codes.NotFound, "no such session id") - } - - result := &IsSessionInCallReply{ - InCall: inCall, - } - return result, nil -} - -func (s *Server) GetInternalSessions(ctx context.Context, request *GetInternalSessionsRequest) (*GetInternalSessionsReply, error) { - statsGrpcServerCalls.WithLabelValues("GetInternalSessions").Inc() - // TODO: Remove debug logging - s.logger.Printf("Get internal sessions from %s on %v (fallback %s)", request.RoomId, request.BackendUrls, request.BackendUrl) // nolint - - var backendUrls []string - if len(request.BackendUrls) > 0 { - backendUrls = request.BackendUrls - } else if request.BackendUrl != "" { // nolint - backendUrls = append(backendUrls, request.BackendUrl) // nolint - } else { - // Only compat backend. - backendUrls = []string{""} - } - - result := &GetInternalSessionsReply{} - processed := make(map[string]bool) - for _, bu := range backendUrls { - var parsed *url.URL - if bu != "" { - var err error - parsed, err = url.Parse(bu) - if err != nil { - return nil, status.Error(codes.InvalidArgument, "invalid url") - } - } - - backend := s.hub.GetBackend(parsed) - if backend == nil { - return nil, status.Error(codes.NotFound, "no such backend") - } - - // Only process each backend once. - if processed[backend.Id()] { - continue - } - processed[backend.Id()] = true - - internalSessions, virtualSessions, found := s.hub.GetInternalSessions(request.RoomId, backend) - if !found { - return nil, status.Error(codes.NotFound, "no such room") - } - - result.InternalSessions = append(result.InternalSessions, internalSessions...) - result.VirtualSessions = append(result.VirtualSessions, virtualSessions...) - } - - return result, nil -} - -func (s *Server) GetPublisherId(ctx context.Context, request *GetPublisherIdRequest) (*GetPublisherIdReply, error) { - statsGrpcServerCalls.WithLabelValues("GetPublisherId").Inc() - // TODO: Remove debug logging - s.logger.Printf("Get %s publisher id for session %s", request.StreamType, request.SessionId) - - return s.hub.GetPublisherIdForSessionId(ctx, api.PublicSessionId(request.SessionId), sfu.StreamType(request.StreamType)) -} - -func (s *Server) GetServerId(ctx context.Context, request *GetServerIdRequest) (*GetServerIdReply, error) { - statsGrpcServerCalls.WithLabelValues("GetServerId").Inc() - return &GetServerIdReply{ - ServerId: s.serverId, - Version: s.version, - }, nil -} - -func (s *Server) GetTransientData(ctx context.Context, request *GetTransientDataRequest) (*GetTransientDataReply, error) { - statsGrpcServerCalls.WithLabelValues("GetTransientData").Inc() - - backendUrls := request.BackendUrls - if len(backendUrls) == 0 { - // Only compat backend. - backendUrls = []string{""} - } - - result := &GetTransientDataReply{} - processed := make(map[string]bool) - for _, bu := range backendUrls { - var parsed *url.URL - if bu != "" { - var err error - parsed, err = url.Parse(bu) - if err != nil { - return nil, status.Error(codes.InvalidArgument, "invalid url") - } - } - - backend := s.hub.GetBackend(parsed) - if backend == nil { - return nil, status.Error(codes.NotFound, "no such backend") - } - - // Only process each backend once. - if processed[backend.Id()] { - continue - } - processed[backend.Id()] = true - - entries, found := s.hub.GetTransientEntries(request.RoomId, backend) - if !found { - return nil, status.Error(codes.NotFound, "no such room") - } else if len(entries) == 0 { - return nil, status.Error(codes.NotFound, "room has no transient data") - } - - if result.Entries == nil { - result.Entries = make(map[string]*GrpcTransientDataEntry) - } - for k, v := range entries { - e := &GrpcTransientDataEntry{} - var err error - if e.Value, err = json.Marshal(v.Value); err != nil { - return nil, status.Errorf(codes.Internal, "error marshalling data: %s", err) - } - if !v.Expires.IsZero() { - e.Expires = v.Expires.UnixMicro() - } - result.Entries[k] = e - } - } - - return result, nil -} - -func (s *Server) GetSessionCount(ctx context.Context, request *GetSessionCountRequest) (*GetSessionCountReply, error) { - statsGrpcServerCalls.WithLabelValues("SessionCount").Inc() - - u, err := url.Parse(request.Url) - if err != nil { - return nil, status.Error(codes.InvalidArgument, "invalid url") - } - - backend := s.hub.GetBackend(u) - if backend == nil { - return nil, status.Error(codes.NotFound, "no such backend") - } - - return &GetSessionCountReply{ - Count: uint32(backend.Len()), - }, nil -} - -func (s *Server) ProxySession(request RpcSessions_ProxySessionServer) error { - statsGrpcServerCalls.WithLabelValues("ProxySession").Inc() - - return s.hub.ProxySession(request) -} diff --git a/grpc/server_id.go b/grpc/server_id.go deleted file mode 100644 index 216ea6c..0000000 --- a/grpc/server_id.go +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package grpc - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "os" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" -) - -var ( - ServerId string -) - -func init() { - hostname, err := os.Hostname() - if err != nil { - hostname = internal.RandomString(8) - } - md := sha256.New() - fmt.Fprintf(md, "%s-%s-%d", internal.RandomString(32), hostname, os.Getpid()) - ServerId = hex.EncodeToString(md.Sum(nil)) -} diff --git a/grpc/server_stats_prometheus.go b/grpc/server_stats_prometheus.go deleted file mode 100644 index 2602fd9..0000000 --- a/grpc/server_stats_prometheus.go +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2022 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package grpc - -import ( - "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" -) - -var ( - statsGrpcServerCalls = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "grpc", - Name: "server_calls_total", - Help: "The total number of GRPC server calls", - }, []string{"method"}) - - grpcServerStats = []prometheus.Collector{ - statsGrpcServerCalls, - } -) - -func RegisterServerStats() { - metrics.RegisterAll(grpcServerStats...) -} diff --git a/grpc/server_test.go b/grpc/server_test.go deleted file mode 100644 index ebec287..0000000 --- a/grpc/server_test.go +++ /dev/null @@ -1,854 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2022 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package grpc - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "errors" - "net" - "net/url" - "path" - "strconv" - "sync/atomic" - "testing" - "time" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/metadata" - status "google.golang.org/grpc/status" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" -) - -type CertificateReloadWaiter interface { - WaitForCertificateReload(ctx context.Context, counter uint64) error -} - -func (s *Server) WaitForCertificateReload(ctx context.Context, counter uint64) error { - c, ok := s.creds.(CertificateReloadWaiter) - if !ok { - return errors.New("no reloadable credentials found") - } - - return c.WaitForCertificateReload(ctx, counter) -} - -type CertPoolReloadWaiter interface { - WaitForCertPoolReload(ctx context.Context, counter uint64) error -} - -func (s *Server) WaitForCertPoolReload(ctx context.Context, counter uint64) error { - c, ok := s.creds.(CertPoolReloadWaiter) - if !ok { - return errors.New("no reloadable credentials found") - } - - return c.WaitForCertPoolReload(ctx, counter) -} - -func NewServerForTestWithConfig(t *testing.T, config *goconf.ConfigFile) (server *Server, addr string) { - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - for port := 50000; port < 50100; port++ { - addr = net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) - config.AddOption("grpc", "listen", addr) - var err error - server, err = NewServer(ctx, config, "0.0.0") - if test.IsErrorAddressAlreadyInUse(err) { - continue - } - - require.NoError(t, err) - break - } - - require.NotNil(t, server, "could not find free port") - - // Don't match with own server id by default. - server.SetServerId("dont-match") - - go func() { - assert.NoError(t, server.Run(), "could not start GRPC server") - }() - - t.Cleanup(func() { - server.Close() - }) - return server, addr -} - -func NewServerForTest(t *testing.T) (server *Server, addr string) { - config := goconf.NewConfigFile() - return NewServerForTestWithConfig(t, config) -} - -func TestServer_ReloadCerts(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - key, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - - org1 := "Testing certificate" - cert1 := internal.GenerateSelfSignedCertificateForTesting(t, org1, key) - - dir := t.TempDir() - privkeyFile := path.Join(dir, "privkey.pem") - pubkeyFile := path.Join(dir, "pubkey.pem") - certFile := path.Join(dir, "cert.pem") - require.NoError(internal.WritePrivateKey(key, privkeyFile)) - require.NoError(internal.WritePublicKey(&key.PublicKey, pubkeyFile)) - require.NoError(internal.WriteCertificate(cert1, certFile)) - - config := goconf.NewConfigFile() - config.AddOption("grpc", "servercertificate", certFile) - config.AddOption("grpc", "serverkey", privkeyFile) - - server, addr := NewServerForTestWithConfig(t, config) - - cp1 := x509.NewCertPool() - cp1.AddCert(cert1) - - cfg1 := &tls.Config{ - RootCAs: cp1, - } - conn1, err := tls.Dial("tcp", addr, cfg1) - require.NoError(err) - defer conn1.Close() // nolint - state1 := conn1.ConnectionState() - if certs := state1.PeerCertificates; assert.NotEmpty(certs) { - if assert.NotEmpty(certs[0].Subject.Organization) { - assert.Equal(org1, certs[0].Subject.Organization[0]) - } - } - - org2 := "Updated certificate" - cert2 := internal.GenerateSelfSignedCertificateForTesting(t, org2, key) - internal.ReplaceCertificate(t, certFile, cert2) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - require.NoError(server.WaitForCertificateReload(ctx, 0)) - - cp2 := x509.NewCertPool() - cp2.AddCert(cert2) - - cfg2 := &tls.Config{ - RootCAs: cp2, - } - conn2, err := tls.Dial("tcp", addr, cfg2) - require.NoError(err) - defer conn2.Close() // nolint - state2 := conn2.ConnectionState() - if certs := state2.PeerCertificates; assert.NotEmpty(certs) { - if assert.NotEmpty(certs[0].Subject.Organization) { - assert.Equal(org2, certs[0].Subject.Organization[0]) - } - } -} - -func TestServer_ReloadCA(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - require := require.New(t) - serverKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - clientKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - - serverCert := internal.GenerateSelfSignedCertificateForTesting(t, "Server cert", serverKey) - org1 := "Testing client" - clientCert1 := internal.GenerateSelfSignedCertificateForTesting(t, org1, clientKey) - - dir := t.TempDir() - privkeyFile := path.Join(dir, "privkey.pem") - pubkeyFile := path.Join(dir, "pubkey.pem") - certFile := path.Join(dir, "cert.pem") - caFile := path.Join(dir, "ca.pem") - require.NoError(internal.WritePrivateKey(serverKey, privkeyFile)) - require.NoError(internal.WritePublicKey(&serverKey.PublicKey, pubkeyFile)) - require.NoError(internal.WriteCertificate(serverCert, certFile)) - require.NoError(internal.WriteCertificate(clientCert1, caFile)) - - config := goconf.NewConfigFile() - config.AddOption("grpc", "servercertificate", certFile) - config.AddOption("grpc", "serverkey", privkeyFile) - config.AddOption("grpc", "clientca", caFile) - - server, addr := NewServerForTestWithConfig(t, config) - - pool := x509.NewCertPool() - pool.AddCert(serverCert) - - pair1, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: clientCert1.Raw, - }), pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(clientKey), - })) - require.NoError(err) - - cfg1 := &tls.Config{ - RootCAs: pool, - Certificates: []tls.Certificate{pair1}, - } - client1, err := NewClient(logger, addr, nil, grpc.WithTransportCredentials(credentials.NewTLS(cfg1))) - require.NoError(err) - defer client1.Close() // nolint - - ctx1, cancel1 := context.WithTimeout(context.Background(), time.Second) - defer cancel1() - - _, _, err = client1.GetServerId(ctx1) - require.NoError(err) - - org2 := "Updated client" - clientCert2 := internal.GenerateSelfSignedCertificateForTesting(t, org2, clientKey) - internal.ReplaceCertificate(t, caFile, clientCert2) - - require.NoError(server.WaitForCertPoolReload(ctx1, 0)) - - pair2, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: clientCert2.Raw, - }), pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(clientKey), - })) - require.NoError(err) - - cfg2 := &tls.Config{ - RootCAs: pool, - Certificates: []tls.Certificate{pair2}, - } - client2, err := NewClient(logger, addr, nil, grpc.WithTransportCredentials(credentials.NewTLS(cfg2))) - require.NoError(err) - defer client2.Close() // nolint - - ctx2, cancel2 := context.WithTimeout(context.Background(), time.Second) - defer cancel2() - - // This will fail if the CA certificate has not been reloaded by the server. - _, _, err = client2.GetServerId(ctx2) - require.NoError(err) -} - -func TestClients_Encryption(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - require := require.New(t) - serverKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - clientKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - - serverCert := internal.GenerateSelfSignedCertificateForTesting(t, "Server cert", serverKey) - clientCert := internal.GenerateSelfSignedCertificateForTesting(t, "Testing client", clientKey) - - dir := t.TempDir() - serverPrivkeyFile := path.Join(dir, "server-privkey.pem") - serverPubkeyFile := path.Join(dir, "server-pubkey.pem") - serverCertFile := path.Join(dir, "server-cert.pem") - require.NoError(internal.WritePrivateKey(serverKey, serverPrivkeyFile)) - require.NoError(internal.WritePublicKey(&serverKey.PublicKey, serverPubkeyFile)) - require.NoError(internal.WriteCertificate(serverCert, serverCertFile)) - clientPrivkeyFile := path.Join(dir, "client-privkey.pem") - clientPubkeyFile := path.Join(dir, "client-pubkey.pem") - clientCertFile := path.Join(dir, "client-cert.pem") - require.NoError(internal.WritePrivateKey(clientKey, clientPrivkeyFile)) - require.NoError(internal.WritePublicKey(&clientKey.PublicKey, clientPubkeyFile)) - require.NoError(internal.WriteCertificate(clientCert, clientCertFile)) - - serverConfig := goconf.NewConfigFile() - serverConfig.AddOption("grpc", "servercertificate", serverCertFile) - serverConfig.AddOption("grpc", "serverkey", serverPrivkeyFile) - serverConfig.AddOption("grpc", "clientca", clientCertFile) - _, addr := NewServerForTestWithConfig(t, serverConfig) - - clientConfig := goconf.NewConfigFile() - clientConfig.AddOption("grpc", "targets", addr) - clientConfig.AddOption("grpc", "clientcertificate", clientCertFile) - clientConfig.AddOption("grpc", "clientkey", clientPrivkeyFile) - clientConfig.AddOption("grpc", "serverca", serverCertFile) - clients, _ := NewClientsForTestWithConfig(t, clientConfig, nil, nil) - - ctx, cancel1 := context.WithTimeout(context.Background(), time.Second) - defer cancel1() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - _, _, err := client.GetServerId(ctx) - require.NoError(err) - } - }) -} - -type disconnectInfo struct { - sessionId api.PublicSessionId - roomSessionId api.RoomSessionId - reason string -} - -type testServerHub struct { - t *testing.T - backend *talk.Backend - - disconnected atomic.Pointer[disconnectInfo] -} - -func newTestServerHub(t *testing.T) *testServerHub { - t.Helper() - logger := logtest.NewLoggerForTest(t) - - cfg := goconf.NewConfigFile() - cfg.AddOption(testBackendId, "secret", "not-so-secret") - cfg.AddOption(testBackendId, "sessionlimit", "10") - backend, err := talk.NewBackendFromConfig(logger, testBackendId, cfg, "foo") - require.NoError(t, err) - - u, err := url.Parse(testBackendUrl) - require.NoError(t, err) - backend.AddUrl(u) - - return &testServerHub{ - t: t, - backend: backend, - } -} - -const ( - testResumeId = "test-resume-id" - testSessionId = "test-session-id" - testRoomSessionId = "test-room-session-id" - testInternalSessionId = "test-internal-session-id" - testVirtualSessionId = "test-virtual-session-id" - testInternalInCallFlags = 2 - testVirtualInCallFlags = 3 - testBackendId = "backend-1" - testBackendUrl = "https://server.domain.invalid" - testRoomId = "test-room-id" - testStreamType = sfu.StreamTypeVideo - testProxyUrl = "https://proxy.domain.invalid" - testIp = "1.2.3.4" - testConnectToken = "test-connection-token" - testPublisherToken = "test-publisher-token" - testAddr = "2.3.4.5" - testCountry = geoip.Country("DE") - testAgent = "test-agent" -) - -var ( - testFeatures = []string{"bar", "foo"} - testExpires = time.Now().Add(time.Minute).Truncate(time.Millisecond) - testMessage = []byte("hello world!") -) - -func (h *testServerHub) GetSessionIdByResumeId(resumeId api.PrivateSessionId) api.PublicSessionId { - if resumeId == testResumeId { - return testSessionId - } - - return "" -} - -func (h *testServerHub) GetSessionIdByRoomSessionId(roomSessionId api.RoomSessionId) (api.PublicSessionId, error) { - if roomSessionId == testRoomSessionId { - return testSessionId, nil - } - - return "", ErrNoSuchRoomSession -} - -func (h *testServerHub) IsSessionIdInCall(sessionId api.PublicSessionId, roomId string, backendUrl string) (bool, bool) { - if roomId == testRoomId && backendUrl == testBackendUrl { - return sessionId == testSessionId, true - } - - return false, false -} - -func (h *testServerHub) DisconnectSessionByRoomSessionId(sessionId api.PublicSessionId, roomSessionId api.RoomSessionId, reason string) { - h.t.Helper() - prev := h.disconnected.Swap(&disconnectInfo{ - sessionId: sessionId, - roomSessionId: roomSessionId, - reason: reason, - }) - assert.Nil(h.t, prev, "duplicate call") -} - -func (h *testServerHub) GetBackend(u *url.URL) *talk.Backend { - if u == nil { - // No compat backend. - return nil - } else if u.String() == testBackendUrl { - return h.backend - } - - return nil -} - -func (h *testServerHub) GetInternalSessions(roomId string, backend *talk.Backend) ([]*InternalSessionData, []*VirtualSessionData, bool) { - if roomId == testRoomId && backend == h.backend { - return []*InternalSessionData{ - { - SessionId: testInternalSessionId, - InCall: testInternalInCallFlags, - Features: testFeatures, - }, - }, []*VirtualSessionData{ - { - SessionId: testVirtualSessionId, - InCall: testVirtualInCallFlags, - }, - }, true - } - - return nil, nil, false -} - -func (h *testServerHub) GetTransientEntries(roomId string, backend *talk.Backend) (api.TransientDataEntries, bool) { - if roomId == testRoomId && backend == h.backend { - return api.TransientDataEntries{ - "foo": api.NewTransientDataEntryWithExpires("bar", testExpires), - "bar": api.NewTransientDataEntry(123, 0), - }, true - } - - return nil, false -} - -func (h *testServerHub) GetPublisherIdForSessionId(ctx context.Context, sessionId api.PublicSessionId, streamType sfu.StreamType) (*GetPublisherIdReply, error) { - if sessionId == testSessionId { - if streamType != testStreamType { - return nil, status.Error(codes.NotFound, "no such publisher") - } - - return &GetPublisherIdReply{ - PublisherId: testSessionId, - ProxyUrl: testProxyUrl, - Ip: testIp, - ConnectToken: testConnectToken, - PublisherToken: testPublisherToken, - }, nil - } - - return nil, status.Error(codes.NotFound, "no such session") -} - -func getMetadata(t *testing.T, md metadata.MD, key string) string { - t.Helper() - if values := md.Get(key); len(values) > 0 { - return values[0] - } - - return "" -} - -func (h *testServerHub) ProxySession(request RpcSessions_ProxySessionServer) error { - h.t.Helper() - if md, found := metadata.FromIncomingContext(request.Context()); assert.True(h.t, found) { - if getMetadata(h.t, md, "sessionId") != testSessionId { - return status.Error(codes.InvalidArgument, "unknown session id") - } - - assert.Equal(h.t, testSessionId, getMetadata(h.t, md, "sessionId")) - assert.Equal(h.t, testAddr, getMetadata(h.t, md, "remoteAddr")) - assert.EqualValues(h.t, testCountry, getMetadata(h.t, md, "country")) - assert.Equal(h.t, testAgent, getMetadata(h.t, md, "userAgent")) - } - - assert.NoError(h.t, request.Send(&ServerSessionMessage{ - Message: testMessage, - })) - - return nil -} - -func TestServer_GetSessionIdByResumeId(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - reply, err := client.LookupResumeId(ctx, "") - assert.ErrorIs(err, ErrNoSuchResumeId, "expected unknown resume id, got %s", reply.GetSessionId()) - - reply, err = client.LookupResumeId(ctx, testResumeId+"1") - assert.ErrorIs(err, ErrNoSuchResumeId, "expected unknown resume id, got %s", reply.GetSessionId()) - - if reply, err := client.LookupResumeId(ctx, testResumeId); assert.NoError(err) { - assert.Equal(testSessionId, reply.SessionId) - } - } -} - -func TestServer_LookupSessionId(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - sessionId, err := client.LookupSessionId(ctx, "", "") - assert.ErrorIs(err, ErrNoSuchRoomSession, "expected unknown room session id, got %s", sessionId) - - sessionId, err = client.LookupSessionId(ctx, testRoomSessionId+"1", "") - assert.ErrorIs(err, ErrNoSuchRoomSession, "expected unknown room session id, got %s", sessionId) - - if sessionId, err := client.LookupSessionId(ctx, testRoomSessionId, "test-reason"); assert.NoError(err) { - assert.EqualValues(testSessionId, sessionId) - } - } - - if disconnected := hub.disconnected.Load(); assert.NotNil(disconnected, "session was not disconnected") { - assert.EqualValues(testSessionId, disconnected.sessionId) - assert.EqualValues(testRoomSessionId, disconnected.roomSessionId) - assert.Equal("test-reason", disconnected.reason) - } -} - -func TestServer_IsSessionInCall(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - if inCall, err := client.IsSessionInCall(ctx, testSessionId, testRoomId+"1", testBackendUrl); assert.NoError(err) { - assert.False(inCall) - } - if inCall, err := client.IsSessionInCall(ctx, testSessionId, testRoomId, testBackendUrl+"1"); assert.NoError(err) { - assert.False(inCall) - } - - if inCall, err := client.IsSessionInCall(ctx, testSessionId+"1", testRoomId, testBackendUrl); assert.NoError(err) { - assert.False(inCall, "should not be in call") - } - if inCall, err := client.IsSessionInCall(ctx, testSessionId, testRoomId, testBackendUrl); assert.NoError(err) { - assert.True(inCall, "should be in call") - } - } -} - -func TestServer_GetInternalSessions(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - if internal, virtual, err := client.GetInternalSessions(ctx, testRoomId+"1", []string{testBackendUrl}); assert.NoError(err) { - assert.Empty(internal) - assert.Empty(virtual) - } - if internal, virtual, err := client.GetInternalSessions(ctx, testRoomId, nil); assert.NoError(err) { - assert.Empty(internal) - assert.Empty(virtual) - } - if internal, virtual, err := client.GetInternalSessions(ctx, testRoomId, []string{testBackendUrl}); assert.NoError(err) { - if assert.Len(internal, 1) && assert.NotNil(internal[testInternalSessionId], "did not find %s in %+v", testInternalSessionId, internal) { - assert.Equal(testInternalSessionId, internal[testInternalSessionId].SessionId) - assert.EqualValues(testInternalInCallFlags, internal[testInternalSessionId].InCall) - assert.Equal(testFeatures, internal[testInternalSessionId].Features) - } - if assert.Len(virtual, 1) && assert.NotNil(virtual[testVirtualSessionId], "did not find %s in %+v", testVirtualSessionId, virtual) { - assert.Equal(testVirtualSessionId, virtual[testVirtualSessionId].SessionId) - assert.EqualValues(testVirtualInCallFlags, virtual[testVirtualSessionId].InCall) - } - } - } -} - -func TestServer_GetPublisherId(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - if publisherId, proxyUrl, ip, connToken, publisherToken, err := client.GetPublisherId(ctx, testSessionId, sfu.StreamTypeVideo); assert.NoError(err) { - assert.EqualValues(testSessionId, publisherId) - assert.Equal(testProxyUrl, proxyUrl) - assert.True(net.ParseIP(testIp).Equal(ip), "expected IP %s, got %s", testIp, ip.String()) - assert.Equal(testConnectToken, connToken) - assert.Equal(testPublisherToken, publisherToken) - } - } -} - -func TestServer_GetTransientData(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - if entries, err := client.GetTransientData(ctx, testRoomId+"1", hub.backend); assert.NoError(err) { - assert.Empty(entries) - } - if entries, err := client.GetTransientData(ctx, testRoomId, hub.backend); assert.NoError(err) && assert.Len(entries, 2) { - if e := entries["foo"]; assert.NotNil(e, "did not find foo in %+v", entries) { - assert.Equal("bar", e.Value) - assert.Equal(testExpires, e.Expires) - } - - if e := entries["bar"]; assert.NotNil(e, "did not find bar in %+v", entries) { - assert.EqualValues(123, e.Value) - assert.True(e.Expires.IsZero(), "should have no expiration, got %s", e.Expires) - } - } - } -} - -type testReceiver struct { - t *testing.T - received atomic.Bool - closed chan struct{} -} - -func (r *testReceiver) RemoteAddr() string { - return testAddr -} - -func (r *testReceiver) Country() geoip.Country { - return testCountry -} - -func (r *testReceiver) UserAgent() string { - return testAgent -} - -func (r *testReceiver) OnProxyMessage(message *ServerSessionMessage) error { - assert.Equal(r.t, testMessage, message.Message) - assert.False(r.t, r.received.Swap(true), "received additional message %v", message) - return nil -} - -func (r *testReceiver) OnProxyClose(err error) { - if err != nil { - if s := status.Convert(err); assert.NotNil(r.t, s, "expected status, got %+v", err) { - assert.Equal(r.t, codes.InvalidArgument, s.Code()) - assert.Equal(r.t, "unknown session id", s.Message()) - } - } - - close(r.closed) -} - -func TestServer_ProxySession(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - receiver := &testReceiver{ - t: t, - closed: make(chan struct{}), - } - if proxy, err := client.ProxySession(ctx, testSessionId, receiver); assert.NoError(err) { - t.Cleanup(func() { - assert.NoError(proxy.Close()) - }) - - assert.NotNil(proxy) - <-receiver.closed - assert.True(receiver.received.Load(), "should have received message") - } - } -} - -func TestServer_ProxySessionError(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - receiver := &testReceiver{ - t: t, - closed: make(chan struct{}), - } - if proxy, err := client.ProxySession(ctx, testSessionId+"1", receiver); assert.NoError(err) { - t.Cleanup(func() { - assert.NoError(proxy.Close()) - }) - - assert.NotNil(proxy) - <-receiver.closed - } - } -} - -type testSession struct{} - -func (s *testSession) PublicId() api.PublicSessionId { - return testSessionId -} - -func (s *testSession) ClientType() api.ClientType { - return api.HelloClientTypeClient -} - -func TestServer_GetSessionCount(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - hub := newTestServerHub(t) - - server, addr := NewServerForTest(t) - server.SetHub(hub) - clients, _ := NewClientsForTest(t, addr, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(clients.WaitForInitialized(ctx)) - - for _, client := range clients.GetClients() { - if count, err := client.GetSessionCount(ctx, testBackendUrl+"1"); assert.NoError(err) { - assert.EqualValues(0, count) - } - if count, err := client.GetSessionCount(ctx, testBackendUrl); assert.NoError(err) { - assert.EqualValues(0, count) - } - assert.NoError(hub.backend.AddSession(&testSession{})) - if count, err := client.GetSessionCount(ctx, testBackendUrl); assert.NoError(err) { - assert.EqualValues(1, count) - } - hub.backend.RemoveSession(&testSession{}) - if count, err := client.GetSessionCount(ctx, testBackendUrl); assert.NoError(err) { - assert.EqualValues(0, count) - } - } -} diff --git a/grpc/sfu.pb.go b/grpc/sfu.pb.go deleted file mode 100644 index 4c4cabf..0000000 --- a/grpc/sfu.pb.go +++ /dev/null @@ -1,238 +0,0 @@ -//* -// Standalone signaling server for the Nextcloud Spreed app. -// Copyright (C) 2022 struktur AG -// -// @author Joachim Bauch -// -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: grpc/sfu.proto - -package grpc - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type GetPublisherIdRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SessionId string `protobuf:"bytes,1,opt,name=sessionId,proto3" json:"sessionId,omitempty"` - StreamType string `protobuf:"bytes,2,opt,name=streamType,proto3" json:"streamType,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetPublisherIdRequest) Reset() { - *x = GetPublisherIdRequest{} - mi := &file_grpc_sfu_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetPublisherIdRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetPublisherIdRequest) ProtoMessage() {} - -func (x *GetPublisherIdRequest) ProtoReflect() protoreflect.Message { - mi := &file_grpc_sfu_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetPublisherIdRequest.ProtoReflect.Descriptor instead. -func (*GetPublisherIdRequest) Descriptor() ([]byte, []int) { - return file_grpc_sfu_proto_rawDescGZIP(), []int{0} -} - -func (x *GetPublisherIdRequest) GetSessionId() string { - if x != nil { - return x.SessionId - } - return "" -} - -func (x *GetPublisherIdRequest) GetStreamType() string { - if x != nil { - return x.StreamType - } - return "" -} - -type GetPublisherIdReply struct { - state protoimpl.MessageState `protogen:"open.v1"` - PublisherId string `protobuf:"bytes,1,opt,name=publisherId,proto3" json:"publisherId,omitempty"` - ProxyUrl string `protobuf:"bytes,2,opt,name=proxyUrl,proto3" json:"proxyUrl,omitempty"` - Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"` - ConnectToken string `protobuf:"bytes,4,opt,name=connectToken,proto3" json:"connectToken,omitempty"` - PublisherToken string `protobuf:"bytes,5,opt,name=publisherToken,proto3" json:"publisherToken,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetPublisherIdReply) Reset() { - *x = GetPublisherIdReply{} - mi := &file_grpc_sfu_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetPublisherIdReply) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetPublisherIdReply) ProtoMessage() {} - -func (x *GetPublisherIdReply) ProtoReflect() protoreflect.Message { - mi := &file_grpc_sfu_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetPublisherIdReply.ProtoReflect.Descriptor instead. -func (*GetPublisherIdReply) Descriptor() ([]byte, []int) { - return file_grpc_sfu_proto_rawDescGZIP(), []int{1} -} - -func (x *GetPublisherIdReply) GetPublisherId() string { - if x != nil { - return x.PublisherId - } - return "" -} - -func (x *GetPublisherIdReply) GetProxyUrl() string { - if x != nil { - return x.ProxyUrl - } - return "" -} - -func (x *GetPublisherIdReply) GetIp() string { - if x != nil { - return x.Ip - } - return "" -} - -func (x *GetPublisherIdReply) GetConnectToken() string { - if x != nil { - return x.ConnectToken - } - return "" -} - -func (x *GetPublisherIdReply) GetPublisherToken() string { - if x != nil { - return x.PublisherToken - } - return "" -} - -var File_grpc_sfu_proto protoreflect.FileDescriptor - -const file_grpc_sfu_proto_rawDesc = "" + - "\n" + - "\x0egrpc/sfu.proto\x12\x04grpc\"U\n" + - "\x15GetPublisherIdRequest\x12\x1c\n" + - "\tsessionId\x18\x01 \x01(\tR\tsessionId\x12\x1e\n" + - "\n" + - "streamType\x18\x02 \x01(\tR\n" + - "streamType\"\xaf\x01\n" + - "\x13GetPublisherIdReply\x12 \n" + - "\vpublisherId\x18\x01 \x01(\tR\vpublisherId\x12\x1a\n" + - "\bproxyUrl\x18\x02 \x01(\tR\bproxyUrl\x12\x0e\n" + - "\x02ip\x18\x03 \x01(\tR\x02ip\x12\"\n" + - "\fconnectToken\x18\x04 \x01(\tR\fconnectToken\x12&\n" + - "\x0epublisherToken\x18\x05 \x01(\tR\x0epublisherToken2T\n" + - "\x06RpcMcu\x12J\n" + - "\x0eGetPublisherId\x12\x1b.grpc.GetPublisherIdRequest\x1a\x19.grpc.GetPublisherIdReply\"\x00B7Z5github.com/strukturag/nextcloud-spreed-signaling/grpcb\x06proto3" - -var ( - file_grpc_sfu_proto_rawDescOnce sync.Once - file_grpc_sfu_proto_rawDescData []byte -) - -func file_grpc_sfu_proto_rawDescGZIP() []byte { - file_grpc_sfu_proto_rawDescOnce.Do(func() { - file_grpc_sfu_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_grpc_sfu_proto_rawDesc), len(file_grpc_sfu_proto_rawDesc))) - }) - return file_grpc_sfu_proto_rawDescData -} - -var file_grpc_sfu_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_grpc_sfu_proto_goTypes = []any{ - (*GetPublisherIdRequest)(nil), // 0: grpc.GetPublisherIdRequest - (*GetPublisherIdReply)(nil), // 1: grpc.GetPublisherIdReply -} -var file_grpc_sfu_proto_depIdxs = []int32{ - 0, // 0: grpc.RpcMcu.GetPublisherId:input_type -> grpc.GetPublisherIdRequest - 1, // 1: grpc.RpcMcu.GetPublisherId:output_type -> grpc.GetPublisherIdReply - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_grpc_sfu_proto_init() } -func file_grpc_sfu_proto_init() { - if File_grpc_sfu_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_grpc_sfu_proto_rawDesc), len(file_grpc_sfu_proto_rawDesc)), - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_grpc_sfu_proto_goTypes, - DependencyIndexes: file_grpc_sfu_proto_depIdxs, - MessageInfos: file_grpc_sfu_proto_msgTypes, - }.Build() - File_grpc_sfu_proto = out.File - file_grpc_sfu_proto_goTypes = nil - file_grpc_sfu_proto_depIdxs = nil -} diff --git a/grpc/test/client.go b/grpc/test/client.go deleted file mode 100644 index fcb22f9..0000000 --- a/grpc/test/client.go +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "testing" - "time" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" - dnstest "github.com/strukturag/nextcloud-spreed-signaling/v2/dns/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" -) - -func NewClientsForTestWithConfig(t *testing.T, config *goconf.ConfigFile, etcdClient etcd.Client, lookup *dnstest.MockLookup) (*grpc.Clients, *dns.Monitor) { - dnsMonitor := dnstest.NewMonitorForTest(t, time.Hour, lookup) // will be updated manually - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - client, err := grpc.NewClients(ctx, config, etcdClient, dnsMonitor, "0.0.0") - require.NoError(t, err) - t.Cleanup(func() { - client.Close() - }) - - return client, dnsMonitor -} - -func NewClientsForTest(t *testing.T, addr string, lookup *dnstest.MockLookup) (*grpc.Clients, *dns.Monitor) { - config := goconf.NewConfigFile() - config.AddOption("grpc", "targets", addr) - config.AddOption("grpc", "dnsdiscovery", "true") - - return NewClientsForTestWithConfig(t, config, nil, lookup) -} - -func NewClientsWithEtcdForTest(t *testing.T, embedEtcd *etcdtest.EtcdServer, lookup *dnstest.MockLookup) (*grpc.Clients, *dns.Monitor) { - config := goconf.NewConfigFile() - config.AddOption("etcd", "endpoints", embedEtcd.URL().String()) - - config.AddOption("grpc", "targettype", "etcd") - config.AddOption("grpc", "targetprefix", "/grpctargets") - - logger := logtest.NewLoggerForTest(t) - etcdClient, err := etcd.NewClient(logger, config, "") - require.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, etcdClient.Close()) - }) - - return NewClientsForTestWithConfig(t, config, etcdClient, lookup) -} diff --git a/grpc/test/client_test.go b/grpc/test/client_test.go deleted file mode 100644 index 5f5c7f2..0000000 --- a/grpc/test/client_test.go +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" -) - -func TestClientsWithEtcd(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - serverId := "the-test-server-id" - server, addr := NewServerForTest(t) - server.SetServerId(serverId) - - etcd := etcdtest.NewServerForTest(t) - etcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr+"\"}")) - - clients, _ := NewClientsWithEtcdForTest(t, etcd, nil) - - require.NoError(clients.WaitForInitialized(t.Context())) - - for _, client := range clients.GetClients() { - if id, version, err := client.GetServerId(t.Context()); assert.NoError(err) { - assert.Equal(serverId, id) - assert.NotEmpty(version) - } - } -} diff --git a/grpc/test/server.go b/grpc/test/server.go deleted file mode 100644 index edb55cb..0000000 --- a/grpc/test/server.go +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "errors" - "net" - "net/url" - "strconv" - "testing" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" -) - -func NewServerForTestWithConfig(t *testing.T, config *goconf.ConfigFile) (server *grpc.Server, addr string) { - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - for port := 50000; port < 50100; port++ { - addr = net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) - config.AddOption("grpc", "listen", addr) - var err error - server, err = grpc.NewServer(ctx, config, "0.0.0") - if test.IsErrorAddressAlreadyInUse(err) { - continue - } - - require.NoError(t, err) - break - } - - require.NotNil(t, server, "could not find free port") - - // Don't match with own server id by default. - server.SetServerId("dont-match") - - go func() { - assert.NoError(t, server.Run(), "could not start GRPC server") - }() - - t.Cleanup(func() { - server.Close() - }) - return server, addr -} - -func NewServerForTest(t *testing.T) (server *grpc.Server, addr string) { - config := goconf.NewConfigFile() - return NewServerForTestWithConfig(t, config) -} - -type MockHub struct { -} - -func (h *MockHub) GetSessionIdByResumeId(resumeId api.PrivateSessionId) api.PublicSessionId { - return "" -} - -func (h *MockHub) GetSessionIdByRoomSessionId(roomSessionId api.RoomSessionId) (api.PublicSessionId, error) { - return "", errors.New("not implemented") -} - -func (h *MockHub) IsSessionIdInCall(sessionId api.PublicSessionId, roomId string, backendUrl string) (bool, bool) { - return false, false -} - -func (h *MockHub) DisconnectSessionByRoomSessionId(sessionId api.PublicSessionId, roomSessionId api.RoomSessionId, reason string) { -} - -func (h *MockHub) GetBackend(u *url.URL) *talk.Backend { - return nil -} - -func (h *MockHub) GetInternalSessions(roomId string, backend *talk.Backend) ([]*grpc.InternalSessionData, []*grpc.VirtualSessionData, bool) { - return nil, nil, false -} - -func (h *MockHub) GetTransientEntries(roomId string, backend *talk.Backend) (api.TransientDataEntries, bool) { - return nil, false -} - -func (h *MockHub) GetPublisherIdForSessionId(ctx context.Context, sessionId api.PublicSessionId, streamType sfu.StreamType) (*grpc.GetPublisherIdReply, error) { - return nil, errors.New("not implemented") -} - -func (h *MockHub) ProxySession(request grpc.RpcSessions_ProxySessionServer) error { - return errors.New("not implemented") -} - -var ( - // Compile-time check that MockHub implements the interface. - _ grpc.ServerHub = &MockHub{} -) diff --git a/grpc/test/server_test.go b/grpc/test/server_test.go deleted file mode 100644 index 03cf00a..0000000 --- a/grpc/test/server_test.go +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -type emptyReceiver struct { -} - -func (r *emptyReceiver) RemoteAddr() string { - return "127.0.0.1" -} - -func (r *emptyReceiver) Country() geoip.Country { - return "DE" -} - -func (r *emptyReceiver) UserAgent() string { - return "testing" -} - -func (r *emptyReceiver) OnProxyMessage(message *grpc.ServerSessionMessage) error { - return errors.New("not implemented") -} - -func (r *emptyReceiver) OnProxyClose(err error) { - // Ignore -} - -func TestServer(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - serverId := "the-test-server-id" - server, addr := NewServerForTest(t) - server.SetServerId(serverId) - hub := &MockHub{} - server.SetHub(hub) - - clients, _ := NewClientsForTest(t, addr, nil) - - require.NoError(clients.WaitForInitialized(t.Context())) - - backend := talk.NewCompatBackend(nil) - - for _, client := range clients.GetClients() { - if id, version, err := client.GetServerId(t.Context()); assert.NoError(err) { - assert.Equal(serverId, id) - assert.NotEmpty(version) - } - - reply, err := client.LookupResumeId(t.Context(), "resume-id") - assert.ErrorIs(err, grpc.ErrNoSuchResumeId) - assert.Nil(reply) - - id, err := client.LookupSessionId(t.Context(), "session-id", "") - if s, ok := status.FromError(err); assert.True(ok) { - assert.Equal(codes.Unknown, s.Code()) - assert.Equal("not implemented", s.Message()) - } - assert.Empty(id) - - if incall, err := client.IsSessionInCall(t.Context(), "session-id", "room-id", ""); assert.NoError(err) { - assert.False(incall) - } - - if internal, virtual, err := client.GetInternalSessions(t.Context(), "room-id", nil); assert.NoError(err) { - assert.Empty(internal) - assert.Empty(virtual) - } - - publisherId, proxyUrl, ip, connToken, publisherToken, err := client.GetPublisherId(t.Context(), "session-id", sfu.StreamTypeVideo) - if s, ok := status.FromError(err); assert.True(ok) { - assert.Equal(codes.Unknown, s.Code()) - assert.Equal("not implemented", s.Message()) - } - assert.Empty(publisherId) - assert.Empty(proxyUrl) - assert.Empty(ip) - assert.Empty(connToken) - assert.Empty(publisherToken) - - if count, err := client.GetSessionCount(t.Context(), ""); assert.NoError(err) { - assert.EqualValues(0, count) - } - - if data, err := client.GetTransientData(t.Context(), "room-id", backend); assert.NoError(err) { - assert.Empty(data) - } - - receiver := &emptyReceiver{} - proxy, err := client.ProxySession(t.Context(), "session-id", receiver) - assert.NoError(err) - assert.NotNil(proxy) - } -} diff --git a/grpc/backend.pb.go b/grpc_backend.pb.go similarity index 68% rename from grpc/backend.pb.go rename to grpc_backend.pb.go index d977af1..03c7762 100644 --- a/grpc/backend.pb.go +++ b/grpc_backend.pb.go @@ -20,16 +20,15 @@ // along with this program. If not, see . // Code generated by protoc-gen-go. DO NOT EDIT. -// source: grpc/backend.proto +// source: grpc_backend.proto -package grpc +package signaling import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" - unsafe "unsafe" ) const ( @@ -129,37 +128,48 @@ func (x *GetSessionCountReply) GetCount() uint32 { var File_grpc_backend_proto protoreflect.FileDescriptor -const file_grpc_backend_proto_rawDesc = "" + - "\n" + - "\x12grpc/backend.proto\x12\x04grpc\"*\n" + - "\x16GetSessionCountRequest\x12\x10\n" + - "\x03url\x18\x01 \x01(\tR\x03url\",\n" + - "\x14GetSessionCountReply\x12\x14\n" + - "\x05count\x18\x01 \x01(\rR\x05count2[\n" + - "\n" + - "RpcBackend\x12M\n" + - "\x0fGetSessionCount\x12\x1c.grpc.GetSessionCountRequest\x1a\x1a.grpc.GetSessionCountReply\"\x00B7Z5github.com/strukturag/nextcloud-spreed-signaling/grpcb\x06proto3" +var file_grpc_backend_proto_rawDesc = []byte{ + 0x0a, 0x12, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x22, + 0x2a, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x2c, 0x0a, 0x14, 0x47, + 0x65, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, + 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x32, 0x65, 0x0a, 0x0a, 0x52, 0x70, 0x63, + 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x57, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x2e, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, + 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, + 0x74, 0x72, 0x75, 0x6b, 0x74, 0x75, 0x72, 0x61, 0x67, 0x2f, 0x6e, 0x65, 0x78, 0x74, 0x63, 0x6c, + 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x70, 0x72, 0x65, 0x65, 0x64, 0x2d, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x6c, 0x69, 0x6e, 0x67, 0x3b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} var ( file_grpc_backend_proto_rawDescOnce sync.Once - file_grpc_backend_proto_rawDescData []byte + file_grpc_backend_proto_rawDescData = file_grpc_backend_proto_rawDesc ) func file_grpc_backend_proto_rawDescGZIP() []byte { file_grpc_backend_proto_rawDescOnce.Do(func() { - file_grpc_backend_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_grpc_backend_proto_rawDesc), len(file_grpc_backend_proto_rawDesc))) + file_grpc_backend_proto_rawDescData = protoimpl.X.CompressGZIP(file_grpc_backend_proto_rawDescData) }) return file_grpc_backend_proto_rawDescData } var file_grpc_backend_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_grpc_backend_proto_goTypes = []any{ - (*GetSessionCountRequest)(nil), // 0: grpc.GetSessionCountRequest - (*GetSessionCountReply)(nil), // 1: grpc.GetSessionCountReply + (*GetSessionCountRequest)(nil), // 0: signaling.GetSessionCountRequest + (*GetSessionCountReply)(nil), // 1: signaling.GetSessionCountReply } var file_grpc_backend_proto_depIdxs = []int32{ - 0, // 0: grpc.RpcBackend.GetSessionCount:input_type -> grpc.GetSessionCountRequest - 1, // 1: grpc.RpcBackend.GetSessionCount:output_type -> grpc.GetSessionCountReply + 0, // 0: signaling.RpcBackend.GetSessionCount:input_type -> signaling.GetSessionCountRequest + 1, // 1: signaling.RpcBackend.GetSessionCount:output_type -> signaling.GetSessionCountReply 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -176,7 +186,7 @@ func file_grpc_backend_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_grpc_backend_proto_rawDesc), len(file_grpc_backend_proto_rawDesc)), + RawDescriptor: file_grpc_backend_proto_rawDesc, NumEnums: 0, NumMessages: 2, NumExtensions: 0, @@ -187,6 +197,7 @@ func file_grpc_backend_proto_init() { MessageInfos: file_grpc_backend_proto_msgTypes, }.Build() File_grpc_backend_proto = out.File + file_grpc_backend_proto_rawDesc = nil file_grpc_backend_proto_goTypes = nil file_grpc_backend_proto_depIdxs = nil } diff --git a/grpc/common_test.go b/grpc_backend.proto similarity index 71% rename from grpc/common_test.go rename to grpc_backend.proto index 2b3d1ed..f667f12 100644 --- a/grpc/common_test.go +++ b/grpc_backend.proto @@ -19,12 +19,20 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package grpc + syntax = "proto3"; -import ( - "time" -) + option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling"; -const ( - testTimeout = 10 * time.Second -) + package signaling; + + service RpcBackend { + rpc GetSessionCount(GetSessionCountRequest) returns (GetSessionCountReply) {} + } + + message GetSessionCountRequest { + string url = 1; + } + + message GetSessionCountReply { + uint32 count = 1; + } diff --git a/grpc/backend_grpc.pb.go b/grpc_backend_grpc.pb.go similarity index 93% rename from grpc/backend_grpc.pb.go rename to grpc_backend_grpc.pb.go index 3d84ced..9f690fc 100644 --- a/grpc/backend_grpc.pb.go +++ b/grpc_backend_grpc.pb.go @@ -20,9 +20,9 @@ // along with this program. If not, see . // Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// source: grpc/backend.proto +// source: grpc_backend.proto -package grpc +package signaling import ( context "context" @@ -37,7 +37,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - RpcBackend_GetSessionCount_FullMethodName = "/grpc.RpcBackend/GetSessionCount" + RpcBackend_GetSessionCount_FullMethodName = "/signaling.RpcBackend/GetSessionCount" ) // RpcBackendClient is the client API for RpcBackend service. @@ -81,7 +81,7 @@ type RpcBackendServer interface { type UnimplementedRpcBackendServer struct{} func (UnimplementedRpcBackendServer) GetSessionCount(context.Context, *GetSessionCountRequest) (*GetSessionCountReply, error) { - return nil, status.Error(codes.Unimplemented, "method GetSessionCount not implemented") + return nil, status.Errorf(codes.Unimplemented, "method GetSessionCount not implemented") } func (UnimplementedRpcBackendServer) mustEmbedUnimplementedRpcBackendServer() {} func (UnimplementedRpcBackendServer) testEmbeddedByValue() {} @@ -94,7 +94,7 @@ type UnsafeRpcBackendServer interface { } func RegisterRpcBackendServer(s grpc.ServiceRegistrar, srv RpcBackendServer) { - // If the following call panics, it indicates UnimplementedRpcBackendServer was + // If the following call pancis, it indicates UnimplementedRpcBackendServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -126,7 +126,7 @@ func _RpcBackend_GetSessionCount_Handler(srv interface{}, ctx context.Context, d // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var RpcBackend_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "grpc.RpcBackend", + ServiceName: "signaling.RpcBackend", HandlerType: (*RpcBackendServer)(nil), Methods: []grpc.MethodDesc{ { @@ -135,5 +135,5 @@ var RpcBackend_ServiceDesc = grpc.ServiceDesc{ }, }, Streams: []grpc.StreamDesc{}, - Metadata: "grpc/backend.proto", + Metadata: "grpc_backend.proto", } diff --git a/grpc/client.go b/grpc_client.go similarity index 58% rename from grpc/client.go rename to grpc_client.go index 083903f..1d33e62 100644 --- a/grpc/client.go +++ b/grpc_client.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package grpc +package signaling import ( "context" @@ -27,8 +27,10 @@ import ( "errors" "fmt" "io" + "log" "net" - "slices" + "net/url" + "strings" "sync" "sync/atomic" "time" @@ -37,36 +39,21 @@ import ( clientv3 "go.etcd.io/etcd/client/v3" "google.golang.org/grpc" codes "google.golang.org/grpc/codes" - "google.golang.org/grpc/connectivity" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/grpc/resolver" status "google.golang.org/grpc/status" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) const ( - TargetTypeStatic = "static" - TargetTypeEtcd = "etcd" + GrpcTargetTypeStatic = "static" + GrpcTargetTypeEtcd = "etcd" - DefaultTargetType = TargetTypeStatic - - initialWaitDelay = time.Second - maxWaitDelay = 8 * time.Second + DefaultGrpcTargetType = GrpcTargetTypeStatic ) var ( - ErrNoSuchResumeId = errors.New("unknown resume id") - ErrNoSuchRoomSession = errors.New("unknown room session id") + ErrNoSuchResumeId = fmt.Errorf("unknown resume id") customResolverPrefix atomic.Uint64 ) @@ -82,7 +69,7 @@ type grpcClientImpl struct { RpcSessionsClient } -func newClientImpl(conn grpc.ClientConnInterface) *grpcClientImpl { +func newGrpcClientImpl(conn grpc.ClientConnInterface) *grpcClientImpl { return &grpcClientImpl{ RpcBackendClient: NewRpcBackendClient(conn), RpcInternalClient: NewRpcInternalClient(conn), @@ -91,16 +78,13 @@ func newClientImpl(conn grpc.ClientConnInterface) *grpcClientImpl { } } -type Client struct { - logger log.Logger - ip net.IP - rawTarget string - target string - conn *grpc.ClientConn - impl *grpcClientImpl +type GrpcClient struct { + ip net.IP + target string + conn *grpc.ClientConn + impl *grpcClientImpl - isSelf atomic.Bool - version atomic.Value + isSelf atomic.Bool } type customIpResolver struct { @@ -141,7 +125,7 @@ func (r *customIpResolver) Close() { // Noop } -func NewClient(logger log.Logger, target string, ip net.IP, opts ...grpc.DialOption) (*Client, error) { +func NewGrpcClient(target string, ip net.IP, opts ...grpc.DialOption) (*GrpcClient, error) { var conn *grpc.ClientConn var err error if ip != nil { @@ -166,43 +150,36 @@ func NewClient(logger log.Logger, target string, ip net.IP, opts ...grpc.DialOpt return nil, err } - result := &Client{ - logger: logger, - ip: ip, - rawTarget: target, - target: target, - conn: conn, - impl: newClientImpl(conn), + result := &GrpcClient{ + ip: ip, + target: target, + conn: conn, + impl: newGrpcClientImpl(conn), } if ip != nil { result.target += " (" + ip.String() + ")" } - result.version.Store("") return result, nil } -func (c *Client) Target() string { +func (c *GrpcClient) Target() string { return c.target } -func (c *Client) Version() string { - return c.version.Load().(string) -} - -func (c *Client) Close() error { +func (c *GrpcClient) Close() error { return c.conn.Close() } -func (c *Client) IsSelf() bool { +func (c *GrpcClient) IsSelf() bool { return c.isSelf.Load() } -func (c *Client) SetSelf(self bool) { +func (c *GrpcClient) SetSelf(self bool) { c.isSelf.Store(self) } -func (c *Client) GetServerId(ctx context.Context) (string, string, error) { +func (c *GrpcClient) GetServerId(ctx context.Context) (string, string, error) { statsGrpcClientCalls.WithLabelValues("GetServerId").Inc() response, err := c.impl.GetServerId(ctx, &GetServerIdRequest{}, grpc.WaitForReady(true)) if err != nil { @@ -212,12 +189,12 @@ func (c *Client) GetServerId(ctx context.Context) (string, string, error) { return response.GetServerId(), response.GetVersion(), nil } -func (c *Client) LookupResumeId(ctx context.Context, resumeId api.PrivateSessionId) (*LookupResumeIdReply, error) { +func (c *GrpcClient) LookupResumeId(ctx context.Context, resumeId string) (*LookupResumeIdReply, error) { statsGrpcClientCalls.WithLabelValues("LookupResumeId").Inc() // TODO: Remove debug logging - c.logger.Printf("Lookup resume id %s on %s", resumeId, c.Target()) + log.Printf("Lookup resume id %s on %s", resumeId, c.Target()) response, err := c.impl.LookupResumeId(ctx, &LookupResumeIdRequest{ - ResumeId: string(resumeId), + ResumeId: resumeId, }, grpc.WaitForReady(true)) if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { return nil, ErrNoSuchResumeId @@ -232,12 +209,12 @@ func (c *Client) LookupResumeId(ctx context.Context, resumeId api.PrivateSession return response, nil } -func (c *Client) LookupSessionId(ctx context.Context, roomSessionId api.RoomSessionId, disconnectReason string) (api.PublicSessionId, error) { +func (c *GrpcClient) LookupSessionId(ctx context.Context, roomSessionId string, disconnectReason string) (string, error) { statsGrpcClientCalls.WithLabelValues("LookupSessionId").Inc() // TODO: Remove debug logging - c.logger.Printf("Lookup room session %s on %s", roomSessionId, c.Target()) + log.Printf("Lookup room session %s on %s", roomSessionId, c.Target()) response, err := c.impl.LookupSessionId(ctx, &LookupSessionIdRequest{ - RoomSessionId: string(roomSessionId), + RoomSessionId: roomSessionId, DisconnectReason: disconnectReason, }, grpc.WaitForReady(true)) if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { @@ -251,17 +228,17 @@ func (c *Client) LookupSessionId(ctx context.Context, roomSessionId api.RoomSess return "", ErrNoSuchRoomSession } - return api.PublicSessionId(sessionId), nil + return sessionId, nil } -func (c *Client) IsSessionInCall(ctx context.Context, sessionId api.PublicSessionId, roomId string, backendUrl string) (bool, error) { +func (c *GrpcClient) IsSessionInCall(ctx context.Context, sessionId string, room *Room) (bool, error) { statsGrpcClientCalls.WithLabelValues("IsSessionInCall").Inc() // TODO: Remove debug logging - c.logger.Printf("Check if session %s is in call %s on %s", sessionId, roomId, c.Target()) + log.Printf("Check if session %s is in call %s on %s", sessionId, room.Id(), c.Target()) response, err := c.impl.IsSessionInCall(ctx, &IsSessionInCallRequest{ - SessionId: string(sessionId), - RoomId: roomId, - BackendUrl: backendUrl, + SessionId: sessionId, + RoomId: room.Id(), + BackendUrl: room.Backend().url, }, grpc.WaitForReady(true)) if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { return false, nil @@ -272,18 +249,13 @@ func (c *Client) IsSessionInCall(ctx context.Context, sessionId api.PublicSessio return response.GetInCall(), nil } -func (c *Client) GetInternalSessions(ctx context.Context, roomId string, backendUrls []string) (internal map[api.PublicSessionId]*InternalSessionData, virtual map[api.PublicSessionId]*VirtualSessionData, err error) { +func (c *GrpcClient) GetInternalSessions(ctx context.Context, roomId string, backend *Backend) (internal map[string]*InternalSessionData, virtual map[string]*VirtualSessionData, err error) { statsGrpcClientCalls.WithLabelValues("GetInternalSessions").Inc() // TODO: Remove debug logging - c.logger.Printf("Get internal sessions for %s on %s", roomId, c.Target()) - var backendUrl string - if len(backendUrls) > 0 { - backendUrl = backendUrls[0] - } + log.Printf("Get internal sessions for %s@%s on %s", roomId, backend.Id(), c.Target()) response, err := c.impl.GetInternalSessions(ctx, &GetInternalSessionsRequest{ - RoomId: roomId, - BackendUrl: backendUrl, - BackendUrls: backendUrls, + RoomId: roomId, + BackendUrl: backend.Url(), }, grpc.WaitForReady(true)) if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { return nil, nil, nil @@ -292,44 +264,44 @@ func (c *Client) GetInternalSessions(ctx context.Context, roomId string, backend } if len(response.InternalSessions) > 0 { - internal = make(map[api.PublicSessionId]*InternalSessionData, len(response.InternalSessions)) + internal = make(map[string]*InternalSessionData, len(response.InternalSessions)) for _, s := range response.InternalSessions { - internal[api.PublicSessionId(s.SessionId)] = s + internal[s.SessionId] = s } } if len(response.VirtualSessions) > 0 { - virtual = make(map[api.PublicSessionId]*VirtualSessionData, len(response.VirtualSessions)) + virtual = make(map[string]*VirtualSessionData, len(response.VirtualSessions)) for _, s := range response.VirtualSessions { - virtual[api.PublicSessionId(s.SessionId)] = s + virtual[s.SessionId] = s } } return } -func (c *Client) GetPublisherId(ctx context.Context, sessionId api.PublicSessionId, streamType sfu.StreamType) (api.PublicSessionId, string, net.IP, string, string, error) { +func (c *GrpcClient) GetPublisherId(ctx context.Context, sessionId string, streamType StreamType) (string, string, net.IP, error) { statsGrpcClientCalls.WithLabelValues("GetPublisherId").Inc() // TODO: Remove debug logging - c.logger.Printf("Get %s publisher id %s on %s", streamType, sessionId, c.Target()) + log.Printf("Get %s publisher id %s on %s", streamType, sessionId, c.Target()) response, err := c.impl.GetPublisherId(ctx, &GetPublisherIdRequest{ - SessionId: string(sessionId), + SessionId: sessionId, StreamType: string(streamType), }, grpc.WaitForReady(true)) if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { - return "", "", nil, "", "", nil + return "", "", nil, nil } else if err != nil { - return "", "", nil, "", "", err + return "", "", nil, err } - return api.PublicSessionId(response.GetPublisherId()), response.GetProxyUrl(), net.ParseIP(response.GetIp()), response.GetConnectToken(), response.GetPublisherToken(), nil + return response.GetPublisherId(), response.GetProxyUrl(), net.ParseIP(response.GetIp()), nil } -func (c *Client) GetSessionCount(ctx context.Context, url string) (uint32, error) { +func (c *GrpcClient) GetSessionCount(ctx context.Context, u *url.URL) (uint32, error) { statsGrpcClientCalls.WithLabelValues("GetSessionCount").Inc() // TODO: Remove debug logging - c.logger.Printf("Get session count for %s on %s", url, c.Target()) + log.Printf("Get session count for %s on %s", u, c.Target()) response, err := c.impl.GetSessionCount(ctx, &GetSessionCountRequest{ - Url: url, + Url: u.String(), }, grpc.WaitForReady(true)) if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { return 0, nil @@ -340,43 +312,9 @@ func (c *Client) GetSessionCount(ctx context.Context, url string) (uint32, error return response.GetCount(), nil } -func (c *Client) GetTransientData(ctx context.Context, roomId string, backend *talk.Backend) (api.TransientDataEntries, error) { - statsGrpcClientCalls.WithLabelValues("GetTransientData").Inc() - // TODO: Remove debug logging - c.logger.Printf("Get transient data for %s@%s on %s", roomId, backend.Id(), c.Target()) - response, err := c.impl.GetTransientData(ctx, &GetTransientDataRequest{ - RoomId: roomId, - BackendUrls: backend.Urls(), - }, grpc.WaitForReady(true)) - if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { - return nil, nil - } else if err != nil { - return nil, err - } - - entries := response.GetEntries() - if len(entries) == 0 { - return nil, nil - } - - result := make(api.TransientDataEntries, len(entries)) - for k, v := range entries { - var value any - if err := json.Unmarshal(v.Value, &value); err != nil { - return nil, err - } - if v.Expires > 0 { - result[k] = api.NewTransientDataEntryWithExpires(value, time.UnixMicro(v.Expires)) - } else { - result[k] = api.NewTransientDataEntry(value, 0) - } - } - return result, nil -} - type ProxySessionReceiver interface { RemoteAddr() string - Country() geoip.Country + Country() string UserAgent() string OnProxyMessage(message *ServerSessionMessage) error @@ -384,8 +322,7 @@ type ProxySessionReceiver interface { } type SessionProxy struct { - logger log.Logger - sessionId api.PublicSessionId + sessionId string receiver ProxySessionReceiver sendMu sync.Mutex @@ -397,7 +334,7 @@ func (p *SessionProxy) recvPump() { defer func() { p.receiver.OnProxyClose(closeError) if err := p.Close(); err != nil { - p.logger.Printf("Error closing proxy for session %s: %s", p.sessionId, err) + log.Printf("Error closing proxy for session %s: %s", p.sessionId, err) } }() @@ -408,13 +345,13 @@ func (p *SessionProxy) recvPump() { break } - p.logger.Printf("Error receiving message from proxy for session %s: %s", p.sessionId, err) + log.Printf("Error receiving message from proxy for session %s: %s", p.sessionId, err) closeError = err break } if err := p.receiver.OnProxyMessage(msg); err != nil { - p.logger.Printf("Error processing message %+v from proxy for session %s: %s", msg, p.sessionId, err) + log.Printf("Error processing message %+v from proxy for session %s: %s", msg, p.sessionId, err) } } } @@ -431,12 +368,12 @@ func (p *SessionProxy) Close() error { return p.client.CloseSend() } -func (c *Client) ProxySession(ctx context.Context, sessionId api.PublicSessionId, receiver ProxySessionReceiver) (*SessionProxy, error) { +func (c *GrpcClient) ProxySession(ctx context.Context, sessionId string, receiver ProxySessionReceiver) (*SessionProxy, error) { statsGrpcClientCalls.WithLabelValues("ProxySession").Inc() md := metadata.Pairs( - "sessionId", string(sessionId), + "sessionId", sessionId, "remoteAddr", receiver.RemoteAddr(), - "country", string(receiver.Country()), + "country", receiver.Country(), "userAgent", receiver.UserAgent(), ) client, err := c.impl.ProxySession(metadata.NewOutgoingContext(ctx, md), grpc.WaitForReady(true)) @@ -445,7 +382,6 @@ func (c *Client) ProxySession(ctx context.Context, sessionId api.PublicSessionId } proxy := &SessionProxy{ - logger: c.logger, sessionId: sessionId, receiver: receiver, @@ -456,29 +392,24 @@ func (c *Client) ProxySession(ctx context.Context, sessionId api.PublicSessionId return proxy, nil } -type clientsList struct { - clients []*Client - entry *dns.MonitorEntry +type grpcClientsList struct { + clients []*GrpcClient + entry *DnsMonitorEntry } -type Clients struct { +type GrpcClients struct { mu sync.RWMutex version string - logger log.Logger - // +checklocks:mu - clientsMap map[string]*clientsList - // +checklocks:mu - clients []*Client + clientsMap map[string]*grpcClientsList + clients []*GrpcClient - dnsMonitor *dns.Monitor - // +checklocks:mu + dnsMonitor *DnsMonitor dnsDiscovery bool - etcdClient etcd.Client // +checklocksignore: Only written to from constructor. - targetPrefix string - // +checklocks:mu - targetInformation map[string]*TargetInformationEtcd + etcdClient *EtcdClient + targetPrefix string + targetInformation map[string]*GrpcTargetInformationEtcd dialOptions atomic.Value // []grpc.DialOption creds credentials.TransportCredentials @@ -488,15 +419,14 @@ type Clients struct { selfCheckWaitGroup sync.WaitGroup closeCtx context.Context - closeFunc context.CancelFunc // +checklocksignore: No locking necessary. + closeFunc context.CancelFunc } -func NewClients(ctx context.Context, config *goconf.ConfigFile, etcdClient etcd.Client, dnsMonitor *dns.Monitor, version string) (*Clients, error) { +func NewGrpcClients(config *goconf.ConfigFile, etcdClient *EtcdClient, dnsMonitor *DnsMonitor, version string) (*GrpcClients, error) { initializedCtx, initializedFunc := context.WithCancel(context.Background()) closeCtx, closeFunc := context.WithCancel(context.Background()) - result := &Clients{ + result := &GrpcClients{ version: version, - logger: log.LoggerFromContext(ctx), dnsMonitor: dnsMonitor, etcdClient: etcdClient, initializedCtx: initializedCtx, @@ -510,34 +440,8 @@ func NewClients(ctx context.Context, config *goconf.ConfigFile, etcdClient etcd. return result, nil } -func (c *Clients) GetServerInfoGrpc() (result []talk.BackendServerInfoGrpc) { - c.mu.RLock() - defer c.mu.RUnlock() - - for _, client := range c.clients { - if client.IsSelf() { - continue - } - - grpc := talk.BackendServerInfoGrpc{ - Target: client.rawTarget, - } - if len(client.ip) > 0 { - grpc.IP = client.ip.String() - } - if client.conn.GetState() == connectivity.Ready { - grpc.Connected = true - grpc.Version = client.Version() - } - - result = append(result, grpc) - } - - return -} - -func (c *Clients) load(config *goconf.ConfigFile, fromReload bool) error { - creds, err := NewReloadableCredentials(c.logger, config, false) +func (c *GrpcClients) load(config *goconf.ConfigFile, fromReload bool) error { + creds, err := NewReloadableCredentials(config, false) if err != nil { return err } @@ -554,13 +458,13 @@ func (c *Clients) load(config *goconf.ConfigFile, fromReload bool) error { targetType, _ := config.GetString("grpc", "targettype") if targetType == "" { - targetType = DefaultTargetType + targetType = DefaultGrpcTargetType } switch targetType { - case TargetTypeStatic: + case GrpcTargetTypeStatic: err = c.loadTargetsStatic(config, fromReload, opts...) - case TargetTypeEtcd: + case GrpcTargetTypeEtcd: err = c.loadTargetsEtcd(config, fromReload, opts...) default: err = fmt.Errorf("unknown GRPC target type: %s", targetType) @@ -568,18 +472,18 @@ func (c *Clients) load(config *goconf.ConfigFile, fromReload bool) error { return err } -func (c *Clients) closeClient(client *Client) { +func (c *GrpcClients) closeClient(client *GrpcClient) { if client.IsSelf() { // Already closed. return } if err := client.Close(); err != nil { - c.logger.Printf("Error closing client to %s: %s", client.Target(), err) + log.Printf("Error closing client to %s: %s", client.Target(), err) } } -func (c *Clients) isClientAvailable(target string, client *Client) bool { +func (c *GrpcClients) isClientAvailable(target string, client *GrpcClient) bool { c.mu.RLock() defer c.mu.RUnlock() @@ -588,10 +492,16 @@ func (c *Clients) isClientAvailable(target string, client *Client) bool { return false } - return slices.Contains(entries.clients, client) + for _, entry := range entries.clients { + if entry == client { + return true + } + } + + return false } -func (c *Clients) getServerIdWithTimeout(ctx context.Context, client *Client) (string, string, error) { +func (c *GrpcClients) getServerIdWithTimeout(ctx context.Context, client *GrpcClient) (string, string, error) { ctx2, cancel := context.WithTimeout(ctx, time.Second) defer cancel() @@ -599,12 +509,8 @@ func (c *Clients) getServerIdWithTimeout(ctx context.Context, client *Client) (s return id, version, err } -func (c *Clients) WaitForSelfCheck() { - c.selfCheckWaitGroup.Wait() -} - -func (c *Clients) checkIsSelf(ctx context.Context, target string, client *Client) { - backoff, _ := async.NewExponentialBackoff(initialWaitDelay, maxWaitDelay) +func (c *GrpcClients) checkIsSelf(ctx context.Context, target string, client *GrpcClient) { + backoff, _ := NewExponentialBackoff(initialWaitDelay, maxWaitDelay) defer c.selfCheckWaitGroup.Done() loop: @@ -625,28 +531,27 @@ loop: } if status.Code(err) != codes.Canceled { - c.logger.Printf("Error checking GRPC server id of %s, retrying in %s: %s", client.Target(), backoff.NextWait(), err) + log.Printf("Error checking GRPC server id of %s, retrying in %s: %s", client.Target(), backoff.NextWait(), err) } backoff.Wait(ctx) continue } - client.version.Store(version) - if id == ServerId { - c.logger.Printf("GRPC target %s is this server, removing", client.Target()) + if id == GrpcServerId { + log.Printf("GRPC target %s is this server, removing", client.Target()) c.closeClient(client) client.SetSelf(true) } else if version != c.version { - c.logger.Printf("WARNING: Node %s is running different version %s than local node (%s)", client.Target(), version, c.version) + log.Printf("WARNING: Node %s is runing different version %s than local node (%s)", client.Target(), version, c.version) } else { - c.logger.Printf("Checked GRPC server id of %s running version %s", client.Target(), version) + log.Printf("Checked GRPC server id of %s running version %s", client.Target(), version) } break loop } } } -func (c *Clients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, opts ...grpc.DialOption) error { +func (c *GrpcClients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, opts ...grpc.DialOption) error { c.mu.Lock() defer c.mu.Unlock() @@ -663,8 +568,8 @@ func (c *Clients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, c.dnsDiscovery = dnsDiscovery } - clientsMap := make(map[string]*clientsList) - var clients []*Client + clientsMap := make(map[string]*grpcClientsList) + var clients []*GrpcClient removeTargets := make(map[string]bool, len(c.clientsMap)) for target, entries := range c.clientsMap { removeTargets[target] = true @@ -672,7 +577,12 @@ func (c *Clients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, } targets, _ := config.GetString("grpc", "targets") - for target := range internal.SplitEntries(targets, ",") { + for _, target := range strings.Split(targets, ",") { + target = strings.TrimSpace(target) + if target == "" { + continue + } + if entries, found := clientsMap[target]; found { clients = append(clients, entries.clients...) if dnsDiscovery && entries.entry == nil { @@ -699,13 +609,13 @@ func (c *Clients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, return err } - clientsMap[target] = &clientsList{ + clientsMap[target] = &grpcClientsList{ entry: entry, } continue } - client, err := NewClient(c.logger, target, nil, opts...) + client, err := NewGrpcClient(target, nil, opts...) if err != nil { for _, entry := range clientsMap { for _, client := range entry.clients { @@ -723,10 +633,10 @@ func (c *Clients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, c.selfCheckWaitGroup.Add(1) go c.checkIsSelf(c.closeCtx, target, client) - c.logger.Printf("Adding %s as GRPC target", client.Target()) + log.Printf("Adding %s as GRPC target", client.Target()) entry, found := clientsMap[target] if !found { - entry = &clientsList{} + entry = &grpcClientsList{} clientsMap[target] = entry } entry.clients = append(entry.clients, client) @@ -736,7 +646,7 @@ func (c *Clients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, for target := range removeTargets { if entry, found := clientsMap[target]; found { for _, client := range entry.clients { - c.logger.Printf("Deleting GRPC target %s", client.Target()) + log.Printf("Deleting GRPC target %s", client.Target()) c.closeClient(client) } @@ -755,7 +665,7 @@ func (c *Clients) loadTargetsStatic(config *goconf.ConfigFile, fromReload bool, return nil } -func (c *Clients) onLookup(entry *dns.MonitorEntry, all []net.IP, added []net.IP, keep []net.IP, removed []net.IP) { +func (c *GrpcClients) onLookup(entry *DnsMonitorEntry, all []net.IP, added []net.IP, keep []net.IP, removed []net.IP) { c.mu.Lock() defer c.mu.Unlock() @@ -768,12 +678,12 @@ func (c *Clients) onLookup(entry *dns.MonitorEntry, all []net.IP, added []net.IP opts := c.dialOptions.Load().([]grpc.DialOption) mapModified := false - var newClients []*Client + var newClients []*GrpcClient for _, ip := range removed { for _, client := range e.clients { if ip.Equal(client.ip) { mapModified = true - c.logger.Printf("Removing connection to %s", client.Target()) + log.Printf("Removing connection to %s", client.Target()) c.closeClient(client) c.wakeupForTesting() } @@ -789,16 +699,16 @@ func (c *Clients) onLookup(entry *dns.MonitorEntry, all []net.IP, added []net.IP } for _, ip := range added { - client, err := NewClient(c.logger, target, ip, opts...) + client, err := NewGrpcClient(target, ip, opts...) if err != nil { - c.logger.Printf("Error creating client to %s with IP %s: %s", target, ip.String(), err) + log.Printf("Error creating client to %s with IP %s: %s", target, ip.String(), err) continue } c.selfCheckWaitGroup.Add(1) go c.checkIsSelf(c.closeCtx, target, client) - c.logger.Printf("Adding %s as GRPC target", client.Target()) + log.Printf("Adding %s as GRPC target", client.Target()) newClients = append(newClients, client) mapModified = true c.wakeupForTesting() @@ -807,7 +717,7 @@ func (c *Clients) onLookup(entry *dns.MonitorEntry, all []net.IP, added []net.IP if mapModified { c.clientsMap[target].clients = newClients - c.clients = make([]*Client, 0, len(c.clientsMap)) + c.clients = make([]*GrpcClient, 0, len(c.clientsMap)) for _, entry := range c.clientsMap { c.clients = append(c.clients, entry.clients...) } @@ -815,28 +725,25 @@ func (c *Clients) onLookup(entry *dns.MonitorEntry, all []net.IP, added []net.IP } } -func (c *Clients) loadTargetsEtcd(config *goconf.ConfigFile, fromReload bool, opts ...grpc.DialOption) error { - c.mu.Lock() - defer c.mu.Unlock() - +func (c *GrpcClients) loadTargetsEtcd(config *goconf.ConfigFile, fromReload bool, opts ...grpc.DialOption) error { if !c.etcdClient.IsConfigured() { - return errors.New("no etcd endpoints configured") + return fmt.Errorf("No etcd endpoints configured") } targetPrefix, _ := config.GetString("grpc", "targetprefix") if targetPrefix == "" { - return errors.New("no GRPC target prefix configured") + return fmt.Errorf("No GRPC target prefix configured") } c.targetPrefix = targetPrefix if c.targetInformation == nil { - c.targetInformation = make(map[string]*TargetInformationEtcd) + c.targetInformation = make(map[string]*GrpcTargetInformationEtcd) } c.etcdClient.AddListener(c) return nil } -func (c *Clients) EtcdClientCreated(client etcd.Client) { +func (c *GrpcClients) EtcdClientCreated(client *EtcdClient) { go func() { if err := client.WaitForConnection(c.closeCtx); err != nil { if errors.Is(err, context.Canceled) { @@ -846,7 +753,7 @@ func (c *Clients) EtcdClientCreated(client etcd.Client) { panic(err) } - backoff, _ := async.NewExponentialBackoff(initialWaitDelay, maxWaitDelay) + backoff, _ := NewExponentialBackoff(initialWaitDelay, maxWaitDelay) var nextRevision int64 for c.closeCtx.Err() == nil { response, err := c.getGrpcTargets(c.closeCtx, client, c.targetPrefix) @@ -854,9 +761,9 @@ func (c *Clients) EtcdClientCreated(client etcd.Client) { if errors.Is(err, context.Canceled) { return } else if errors.Is(err, context.DeadlineExceeded) { - c.logger.Printf("Timeout getting initial list of GRPC targets, retry in %s", backoff.NextWait()) + log.Printf("Timeout getting initial list of GRPC targets, retry in %s", backoff.NextWait()) } else { - c.logger.Printf("Could not get initial list of GRPC targets, retry in %s: %s", backoff.NextWait(), err) + log.Printf("Could not get initial list of GRPC targets, retry in %s: %s", backoff.NextWait(), err) } backoff.Wait(c.closeCtx) @@ -876,7 +783,7 @@ func (c *Clients) EtcdClientCreated(client etcd.Client) { for c.closeCtx.Err() == nil { var err error if nextRevision, err = client.Watch(c.closeCtx, c.targetPrefix, nextRevision, c, clientv3.WithPrefix()); err != nil { - c.logger.Printf("Error processing watch for %s (%s), retry in %s", c.targetPrefix, err, backoff.NextWait()) + log.Printf("Error processing watch for %s (%s), retry in %s", c.targetPrefix, err, backoff.NextWait()) backoff.Wait(c.closeCtx) continue } @@ -885,31 +792,31 @@ func (c *Clients) EtcdClientCreated(client etcd.Client) { backoff.Reset() prevRevision = nextRevision } else { - c.logger.Printf("Processing watch for %s interrupted, retry in %s", c.targetPrefix, backoff.NextWait()) + log.Printf("Processing watch for %s interrupted, retry in %s", c.targetPrefix, backoff.NextWait()) backoff.Wait(c.closeCtx) } } }() } -func (c *Clients) EtcdWatchCreated(client etcd.Client, key string) { +func (c *GrpcClients) EtcdWatchCreated(client *EtcdClient, key string) { } -func (c *Clients) getGrpcTargets(ctx context.Context, client etcd.Client, targetPrefix string) (*clientv3.GetResponse, error) { +func (c *GrpcClients) getGrpcTargets(ctx context.Context, client *EtcdClient, targetPrefix string) (*clientv3.GetResponse, error) { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() return client.Get(ctx, targetPrefix, clientv3.WithPrefix()) } -func (c *Clients) EtcdKeyUpdated(client etcd.Client, key string, data []byte, prevValue []byte) { - var info TargetInformationEtcd +func (c *GrpcClients) EtcdKeyUpdated(client *EtcdClient, key string, data []byte, prevValue []byte) { + var info GrpcTargetInformationEtcd if err := json.Unmarshal(data, &info); err != nil { - c.logger.Printf("Could not decode GRPC target %s=%s: %s", key, string(data), err) + log.Printf("Could not decode GRPC target %s=%s: %s", key, string(data), err) return } if err := info.CheckValid(); err != nil { - c.logger.Printf("Received invalid GRPC target %s=%s: %s", key, string(data), err) + log.Printf("Received invalid GRPC target %s=%s: %s", key, string(data), err) return } @@ -923,27 +830,27 @@ func (c *Clients) EtcdKeyUpdated(client etcd.Client, key string, data []byte, pr } if _, found := c.clientsMap[info.Address]; found { - c.logger.Printf("GRPC target %s already exists, ignoring %s", info.Address, key) + log.Printf("GRPC target %s already exists, ignoring %s", info.Address, key) return } opts := c.dialOptions.Load().([]grpc.DialOption) - cl, err := NewClient(c.logger, info.Address, nil, opts...) + cl, err := NewGrpcClient(info.Address, nil, opts...) if err != nil { - c.logger.Printf("Could not create GRPC client for target %s: %s", info.Address, err) + log.Printf("Could not create GRPC client for target %s: %s", info.Address, err) return } c.selfCheckWaitGroup.Add(1) go c.checkIsSelf(c.closeCtx, info.Address, cl) - c.logger.Printf("Adding %s as GRPC target", cl.Target()) + log.Printf("Adding %s as GRPC target", cl.Target()) if c.clientsMap == nil { - c.clientsMap = make(map[string]*clientsList) + c.clientsMap = make(map[string]*grpcClientsList) } - c.clientsMap[info.Address] = &clientsList{ - clients: []*Client{cl}, + c.clientsMap[info.Address] = &grpcClientsList{ + clients: []*GrpcClient{cl}, } c.clients = append(c.clients, cl) c.targetInformation[key] = &info @@ -951,18 +858,17 @@ func (c *Clients) EtcdKeyUpdated(client etcd.Client, key string, data []byte, pr c.wakeupForTesting() } -func (c *Clients) EtcdKeyDeleted(client etcd.Client, key string, prevValue []byte) { +func (c *GrpcClients) EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte) { c.mu.Lock() defer c.mu.Unlock() c.removeEtcdClientLocked(key) } -// +checklocks:c.mu -func (c *Clients) removeEtcdClientLocked(key string) { +func (c *GrpcClients) removeEtcdClientLocked(key string) { info, found := c.targetInformation[key] if !found { - c.logger.Printf("No connection found for %s, ignoring", key) + log.Printf("No connection found for %s, ignoring", key) c.wakeupForTesting() return } @@ -974,11 +880,11 @@ func (c *Clients) removeEtcdClientLocked(key string) { } for _, client := range entry.clients { - c.logger.Printf("Removing connection to %s (from %s)", client.Target(), key) + log.Printf("Removing connection to %s (from %s)", client.Target(), key) c.closeClient(client) } delete(c.clientsMap, info.Address) - c.clients = make([]*Client, 0, len(c.clientsMap)) + c.clients = make([]*GrpcClient, 0, len(c.clientsMap)) for _, entry := range c.clientsMap { c.clients = append(c.clients, entry.clients...) } @@ -986,7 +892,7 @@ func (c *Clients) removeEtcdClientLocked(key string) { c.wakeupForTesting() } -func (c *Clients) WaitForInitialized(ctx context.Context) error { +func (c *GrpcClients) WaitForInitialized(ctx context.Context) error { select { case <-ctx.Done(): return ctx.Err() @@ -995,20 +901,7 @@ func (c *Clients) WaitForInitialized(ctx context.Context) error { } } -func (c *Clients) GetWakeupChannelForTesting() <-chan struct{} { - c.mu.Lock() - defer c.mu.Unlock() - - if c.wakeupChanForTesting != nil { - return c.wakeupChanForTesting - } - - ch := make(chan struct{}, 1) - c.wakeupChanForTesting = ch - return ch -} - -func (c *Clients) wakeupForTesting() { +func (c *GrpcClients) wakeupForTesting() { if c.wakeupChanForTesting == nil { return } @@ -1019,20 +912,20 @@ func (c *Clients) wakeupForTesting() { } } -func (c *Clients) Reload(config *goconf.ConfigFile) { +func (c *GrpcClients) Reload(config *goconf.ConfigFile) { if err := c.load(config, true); err != nil { - c.logger.Printf("Could not reload RPC clients: %s", err) + log.Printf("Could not reload RPC clients: %s", err) } } -func (c *Clients) Close() { +func (c *GrpcClients) Close() { c.mu.Lock() defer c.mu.Unlock() for _, entry := range c.clientsMap { for _, client := range entry.clients { if err := client.Close(); err != nil { - c.logger.Printf("Error closing client to %s: %s", client.Target(), err) + log.Printf("Error closing client to %s: %s", client.Target(), err) } } @@ -1057,7 +950,7 @@ func (c *Clients) Close() { c.closeFunc() } -func (c *Clients) GetClients() []*Client { +func (c *GrpcClients) GetClients() []*GrpcClient { c.mu.RLock() defer c.mu.RUnlock() @@ -1065,7 +958,7 @@ func (c *Clients) GetClients() []*Client { return c.clients } - result := make([]*Client, 0, len(c.clients)-1) + result := make([]*GrpcClient, 0, len(c.clients)-1) for _, client := range c.clients { if client.IsSelf() { continue diff --git a/grpc_client_test.go b/grpc_client_test.go new file mode 100644 index 0000000..b536a34 --- /dev/null +++ b/grpc_client_test.go @@ -0,0 +1,343 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "fmt" + "net" + "os" + "path" + "testing" + "time" + + "github.com/dlintw/goconf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.etcd.io/etcd/server/v3/embed" +) + +func (c *GrpcClients) getWakeupChannelForTesting() <-chan struct{} { + c.mu.Lock() + defer c.mu.Unlock() + + if c.wakeupChanForTesting != nil { + return c.wakeupChanForTesting + } + + ch := make(chan struct{}, 1) + c.wakeupChanForTesting = ch + return ch +} + +func NewGrpcClientsForTestWithConfig(t *testing.T, config *goconf.ConfigFile, etcdClient *EtcdClient) (*GrpcClients, *DnsMonitor) { + dnsMonitor := newDnsMonitorForTest(t, time.Hour) // will be updated manually + client, err := NewGrpcClients(config, etcdClient, dnsMonitor, "0.0.0") + require.NoError(t, err) + t.Cleanup(func() { + client.Close() + }) + + return client, dnsMonitor +} + +func NewGrpcClientsForTest(t *testing.T, addr string) (*GrpcClients, *DnsMonitor) { + config := goconf.NewConfigFile() + config.AddOption("grpc", "targets", addr) + config.AddOption("grpc", "dnsdiscovery", "true") + + return NewGrpcClientsForTestWithConfig(t, config, nil) +} + +func NewGrpcClientsWithEtcdForTest(t *testing.T, etcd *embed.Etcd) (*GrpcClients, *DnsMonitor) { + config := goconf.NewConfigFile() + config.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String()) + + config.AddOption("grpc", "targettype", "etcd") + config.AddOption("grpc", "targetprefix", "/grpctargets") + + etcdClient, err := NewEtcdClient(config, "") + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, etcdClient.Close()) + }) + + return NewGrpcClientsForTestWithConfig(t, config, etcdClient) +} + +func drainWakeupChannel(ch <-chan struct{}) { + for { + select { + case <-ch: + default: + return + } + } +} + +func waitForEvent(ctx context.Context, t *testing.T, ch <-chan struct{}) { + t.Helper() + + select { + case <-ch: + return + case <-ctx.Done(): + assert.Fail(t, "timeout waiting for event") + } +} + +func Test_GrpcClients_EtcdInitial(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { + _, addr1 := NewGrpcServerForTest(t) + _, addr2 := NewGrpcServerForTest(t) + + etcd := NewEtcdForTest(t) + + SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) + SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) + + client, _ := NewGrpcClientsWithEtcdForTest(t, etcd) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + require.NoError(t, client.WaitForInitialized(ctx)) + + clients := client.GetClients() + assert.Len(t, clients, 2, "Expected two clients, got %+v", clients) + }) +} + +func Test_GrpcClients_EtcdUpdate(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) + etcd := NewEtcdForTest(t) + client, _ := NewGrpcClientsWithEtcdForTest(t, etcd) + ch := client.getWakeupChannelForTesting() + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + assert.Empty(client.GetClients()) + + drainWakeupChannel(ch) + _, addr1 := NewGrpcServerForTest(t) + SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) + waitForEvent(ctx, t, ch) + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(addr1, clients[0].Target()) + } + + drainWakeupChannel(ch) + _, addr2 := NewGrpcServerForTest(t) + SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) + waitForEvent(ctx, t, ch) + if clients := client.GetClients(); assert.Len(clients, 2) { + assert.Equal(addr1, clients[0].Target()) + assert.Equal(addr2, clients[1].Target()) + } + + drainWakeupChannel(ch) + DeleteEtcdValue(etcd, "/grpctargets/one") + waitForEvent(ctx, t, ch) + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(addr2, clients[0].Target()) + } + + drainWakeupChannel(ch) + _, addr3 := NewGrpcServerForTest(t) + SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr3+"\"}")) + waitForEvent(ctx, t, ch) + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(addr3, clients[0].Target()) + } +} + +func Test_GrpcClients_EtcdIgnoreSelf(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) + etcd := NewEtcdForTest(t) + client, _ := NewGrpcClientsWithEtcdForTest(t, etcd) + ch := client.getWakeupChannelForTesting() + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + assert.Empty(client.GetClients()) + + drainWakeupChannel(ch) + _, addr1 := NewGrpcServerForTest(t) + SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) + waitForEvent(ctx, t, ch) + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(addr1, clients[0].Target()) + } + + drainWakeupChannel(ch) + server2, addr2 := NewGrpcServerForTest(t) + server2.serverId = GrpcServerId + SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) + waitForEvent(ctx, t, ch) + client.selfCheckWaitGroup.Wait() + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(addr1, clients[0].Target()) + } + + drainWakeupChannel(ch) + DeleteEtcdValue(etcd, "/grpctargets/two") + waitForEvent(ctx, t, ch) + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(addr1, clients[0].Target()) + } +} + +func Test_GrpcClients_DnsDiscovery(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { + assert := assert.New(t) + lookup := newMockDnsLookupForTest(t) + target := "testgrpc:12345" + ip1 := net.ParseIP("192.168.0.1") + ip2 := net.ParseIP("192.168.0.2") + targetWithIp1 := fmt.Sprintf("%s (%s)", target, ip1) + targetWithIp2 := fmt.Sprintf("%s (%s)", target, ip2) + lookup.Set("testgrpc", []net.IP{ip1}) + client, dnsMonitor := NewGrpcClientsForTest(t, target) + ch := client.getWakeupChannelForTesting() + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + dnsMonitor.checkHostnames() + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(targetWithIp1, clients[0].Target()) + assert.True(clients[0].ip.Equal(ip1), "Expected IP %s, got %s", ip1, clients[0].ip) + } + + lookup.Set("testgrpc", []net.IP{ip1, ip2}) + drainWakeupChannel(ch) + dnsMonitor.checkHostnames() + waitForEvent(ctx, t, ch) + + if clients := client.GetClients(); assert.Len(clients, 2) { + assert.Equal(targetWithIp1, clients[0].Target()) + assert.True(clients[0].ip.Equal(ip1), "Expected IP %s, got %s", ip1, clients[0].ip) + assert.Equal(targetWithIp2, clients[1].Target()) + assert.True(clients[1].ip.Equal(ip2), "Expected IP %s, got %s", ip2, clients[1].ip) + } + + lookup.Set("testgrpc", []net.IP{ip2}) + drainWakeupChannel(ch) + dnsMonitor.checkHostnames() + waitForEvent(ctx, t, ch) + + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(targetWithIp2, clients[0].Target()) + assert.True(clients[0].ip.Equal(ip2), "Expected IP %s, got %s", ip2, clients[0].ip) + } + }) +} + +func Test_GrpcClients_DnsDiscoveryInitialFailed(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + assert := assert.New(t) + lookup := newMockDnsLookupForTest(t) + target := "testgrpc:12345" + ip1 := net.ParseIP("192.168.0.1") + targetWithIp1 := fmt.Sprintf("%s (%s)", target, ip1) + client, dnsMonitor := NewGrpcClientsForTest(t, target) + ch := client.getWakeupChannelForTesting() + + testCtx, testCtxCancel := context.WithTimeout(context.Background(), testTimeout) + defer testCtxCancel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + require.NoError(t, client.WaitForInitialized(ctx)) + + assert.Empty(client.GetClients()) + + lookup.Set("testgrpc", []net.IP{ip1}) + drainWakeupChannel(ch) + dnsMonitor.checkHostnames() + waitForEvent(testCtx, t, ch) + + if clients := client.GetClients(); assert.Len(clients, 1) { + assert.Equal(targetWithIp1, clients[0].Target()) + assert.True(clients[0].ip.Equal(ip1), "Expected IP %s, got %s", ip1, clients[0].ip) + } +} + +func Test_GrpcClients_Encryption(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { + require := require.New(t) + serverKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(err) + clientKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(err) + + serverCert := GenerateSelfSignedCertificateForTesting(t, 1024, "Server cert", serverKey) + clientCert := GenerateSelfSignedCertificateForTesting(t, 1024, "Testing client", clientKey) + + dir := t.TempDir() + serverPrivkeyFile := path.Join(dir, "server-privkey.pem") + serverPubkeyFile := path.Join(dir, "server-pubkey.pem") + serverCertFile := path.Join(dir, "server-cert.pem") + WritePrivateKey(serverKey, serverPrivkeyFile) // nolint + WritePublicKey(&serverKey.PublicKey, serverPubkeyFile) // nolint + os.WriteFile(serverCertFile, serverCert, 0755) // nolint + clientPrivkeyFile := path.Join(dir, "client-privkey.pem") + clientPubkeyFile := path.Join(dir, "client-pubkey.pem") + clientCertFile := path.Join(dir, "client-cert.pem") + WritePrivateKey(clientKey, clientPrivkeyFile) // nolint + WritePublicKey(&clientKey.PublicKey, clientPubkeyFile) // nolint + os.WriteFile(clientCertFile, clientCert, 0755) // nolint + + serverConfig := goconf.NewConfigFile() + serverConfig.AddOption("grpc", "servercertificate", serverCertFile) + serverConfig.AddOption("grpc", "serverkey", serverPrivkeyFile) + serverConfig.AddOption("grpc", "clientca", clientCertFile) + _, addr := NewGrpcServerForTestWithConfig(t, serverConfig) + + clientConfig := goconf.NewConfigFile() + clientConfig.AddOption("grpc", "targets", addr) + clientConfig.AddOption("grpc", "clientcertificate", clientCertFile) + clientConfig.AddOption("grpc", "clientkey", clientPrivkeyFile) + clientConfig.AddOption("grpc", "serverca", serverCertFile) + clients, _ := NewGrpcClientsForTestWithConfig(t, clientConfig, nil) + + ctx, cancel1 := context.WithTimeout(context.Background(), time.Second) + defer cancel1() + + require.NoError(clients.WaitForInitialized(ctx)) + + for _, client := range clients.GetClients() { + _, _, err := client.GetServerId(ctx) + require.NoError(err) + } + }) +} diff --git a/grpc/common.go b/grpc_common.go similarity index 75% rename from grpc/common.go rename to grpc_common.go index 085b13f..b7df93e 100644 --- a/grpc/common.go +++ b/grpc_common.go @@ -19,29 +19,25 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package grpc +package signaling import ( "context" "crypto/tls" - "errors" "fmt" + "log" "net" - "time" "github.com/dlintw/goconf" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/security" ) type reloadableCredentials struct { config *tls.Config - loader *security.CertificateReloader - pool *security.CertPoolReloader + loader *CertificateReloader + pool *CertPoolReloader } func (c *reloadableCredentials) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { @@ -138,35 +134,7 @@ func (c *reloadableCredentials) Close() { } } -func (c *reloadableCredentials) WaitForCertificateReload(ctx context.Context, counter uint64) error { - if c.loader == nil { - return errors.New("no certificate loaded") - } - - for counter == c.loader.GetReloadCounter() { - if err := ctx.Err(); err != nil { - return err - } - time.Sleep(time.Millisecond) - } - return nil -} - -func (c *reloadableCredentials) WaitForCertPoolReload(ctx context.Context, counter uint64) error { - if c.pool == nil { - return errors.New("no certificate pool loaded") - } - - for counter == c.pool.GetReloadCounter() { - if err := ctx.Err(); err != nil { - return err - } - time.Sleep(time.Millisecond) - } - return nil -} - -func NewReloadableCredentials(logger log.Logger, config *goconf.ConfigFile, server bool) (credentials.TransportCredentials, error) { +func NewReloadableCredentials(config *goconf.ConfigFile, server bool) (credentials.TransportCredentials, error) { var prefix string var caPrefix string if server { @@ -182,18 +150,18 @@ func NewReloadableCredentials(logger log.Logger, config *goconf.ConfigFile, serv cfg := &tls.Config{ NextProtos: []string{"h2"}, } - var loader *security.CertificateReloader + var loader *CertificateReloader var err error if certificateFile != "" && keyFile != "" { - loader, err = security.NewCertificateReloader(logger, certificateFile, keyFile) + loader, err = NewCertificateReloader(certificateFile, keyFile) if err != nil { return nil, fmt.Errorf("invalid GRPC %s certificate / key in %s / %s: %w", prefix, certificateFile, keyFile, err) } } - var pool *security.CertPoolReloader + var pool *CertPoolReloader if caFile != "" { - pool, err = security.NewCertPoolReloader(logger, caFile) + pool, err = NewCertPoolReloader(caFile) if err != nil { return nil, err } @@ -205,9 +173,9 @@ func NewReloadableCredentials(logger log.Logger, config *goconf.ConfigFile, serv if loader == nil && pool == nil { if server { - logger.Printf("WARNING: No GRPC server certificate and/or key configured, running unencrypted") + log.Printf("WARNING: No GRPC server certificate and/or key configured, running unencrypted") } else { - logger.Printf("WARNING: No GRPC CA configured, expecting unencrypted connections") + log.Printf("WARNING: No GRPC CA configured, expecting unencrypted connections") } return insecure.NewCredentials(), nil } diff --git a/internal/crypto_helpers.go b/grpc_common_test.go similarity index 76% rename from internal/crypto_helpers.go rename to grpc_common_test.go index fb4b18c..878efd0 100644 --- a/internal/crypto_helpers.go +++ b/grpc_common_test.go @@ -1,6 +1,6 @@ /** * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG + * Copyright (C) 2022 struktur AG * * @author Joachim Bauch * @@ -19,14 +19,17 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( + "context" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "errors" + "io/fs" "math/big" "net" "os" @@ -36,8 +39,23 @@ import ( "github.com/stretchr/testify/require" ) -func GenerateSelfSignedCertificateForTesting(t *testing.T, organization string, key *rsa.PrivateKey) *x509.Certificate { - t.Helper() +func (c *reloadableCredentials) WaitForCertificateReload(ctx context.Context) error { + if c.loader == nil { + return errors.New("no certificate loaded") + } + + return c.loader.WaitForReload(ctx) +} + +func (c *reloadableCredentials) WaitForCertPoolReload(ctx context.Context) error { + if c.pool == nil { + return errors.New("no certificate pool loaded") + } + + return c.pool.WaitForReload(ctx) +} + +func GenerateSelfSignedCertificateForTesting(t *testing.T, bits int, organization string, key *rsa.PrivateKey) []byte { template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ @@ -52,17 +70,17 @@ func GenerateSelfSignedCertificateForTesting(t *testing.T, organization string, x509.ExtKeyUsageServerAuth, }, BasicConstraintsValid: true, - DNSNames: []string{"localhost"}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, } data, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) require.NoError(t, err) - cert, err := x509.ParseCertificate(data) - require.NoError(t, err) - - return cert + data = pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: data, + }) + return data } func WritePrivateKey(key *rsa.PrivateKey, filename string) error { @@ -88,28 +106,14 @@ func WritePublicKey(key *rsa.PublicKey, filename string) error { return os.WriteFile(filename, data, 0755) } -func WriteCertificate(cert *x509.Certificate, filename string) error { - data := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - }) - - return os.WriteFile(filename, data, 0755) -} - -func ReplaceCertificate(t *testing.T, filename string, cert *x509.Certificate) { +func replaceFile(t *testing.T, filename string, data []byte, perm fs.FileMode) { t.Helper() require := require.New(t) oldStat, err := os.Stat(filename) require.NoError(err, "can't stat old file %s", filename) - data := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - }) - for { - require.NoError(os.WriteFile(filename, data, 0755), "can't write file %s", filename) + require.NoError(os.WriteFile(filename, data, perm), "can't write file %s", filename) newStat, err := os.Stat(filename) require.NoError(err, "can't stat new file %s", filename) diff --git a/grpc_internal.pb.go b/grpc_internal.pb.go new file mode 100644 index 0000000..eee099c --- /dev/null +++ b/grpc_internal.pb.go @@ -0,0 +1,203 @@ +//* +// Standalone signaling server for the Nextcloud Spreed app. +// Copyright (C) 2022 struktur AG +// +// @author Joachim Bauch +// +// @license GNU AGPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: grpc_internal.proto + +package signaling + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetServerIdRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServerIdRequest) Reset() { + *x = GetServerIdRequest{} + mi := &file_grpc_internal_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServerIdRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServerIdRequest) ProtoMessage() {} + +func (x *GetServerIdRequest) ProtoReflect() protoreflect.Message { + mi := &file_grpc_internal_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetServerIdRequest.ProtoReflect.Descriptor instead. +func (*GetServerIdRequest) Descriptor() ([]byte, []int) { + return file_grpc_internal_proto_rawDescGZIP(), []int{0} +} + +type GetServerIdReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServerId string `protobuf:"bytes,1,opt,name=serverId,proto3" json:"serverId,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetServerIdReply) Reset() { + *x = GetServerIdReply{} + mi := &file_grpc_internal_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetServerIdReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServerIdReply) ProtoMessage() {} + +func (x *GetServerIdReply) ProtoReflect() protoreflect.Message { + mi := &file_grpc_internal_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetServerIdReply.ProtoReflect.Descriptor instead. +func (*GetServerIdReply) Descriptor() ([]byte, []int) { + return file_grpc_internal_proto_rawDescGZIP(), []int{1} +} + +func (x *GetServerIdReply) GetServerId() string { + if x != nil { + return x.ServerId + } + return "" +} + +func (x *GetServerIdReply) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +var File_grpc_internal_proto protoreflect.FileDescriptor + +var file_grpc_internal_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, + 0x22, 0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x48, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x49, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x32, 0x5a, 0x0a, 0x0b, 0x52, 0x70, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, + 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, + 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x49, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x3c, 0x5a, 0x3a, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x6b, + 0x74, 0x75, 0x72, 0x61, 0x67, 0x2f, 0x6e, 0x65, 0x78, 0x74, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, + 0x73, 0x70, 0x72, 0x65, 0x65, 0x64, 0x2d, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, + 0x3b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_grpc_internal_proto_rawDescOnce sync.Once + file_grpc_internal_proto_rawDescData = file_grpc_internal_proto_rawDesc +) + +func file_grpc_internal_proto_rawDescGZIP() []byte { + file_grpc_internal_proto_rawDescOnce.Do(func() { + file_grpc_internal_proto_rawDescData = protoimpl.X.CompressGZIP(file_grpc_internal_proto_rawDescData) + }) + return file_grpc_internal_proto_rawDescData +} + +var file_grpc_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_grpc_internal_proto_goTypes = []any{ + (*GetServerIdRequest)(nil), // 0: signaling.GetServerIdRequest + (*GetServerIdReply)(nil), // 1: signaling.GetServerIdReply +} +var file_grpc_internal_proto_depIdxs = []int32{ + 0, // 0: signaling.RpcInternal.GetServerId:input_type -> signaling.GetServerIdRequest + 1, // 1: signaling.RpcInternal.GetServerId:output_type -> signaling.GetServerIdReply + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_grpc_internal_proto_init() } +func file_grpc_internal_proto_init() { + if File_grpc_internal_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_grpc_internal_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_grpc_internal_proto_goTypes, + DependencyIndexes: file_grpc_internal_proto_depIdxs, + MessageInfos: file_grpc_internal_proto_msgTypes, + }.Build() + File_grpc_internal_proto = out.File + file_grpc_internal_proto_rawDesc = nil + file_grpc_internal_proto_goTypes = nil + file_grpc_internal_proto_depIdxs = nil +} diff --git a/grpc/backend.proto b/grpc_internal.proto similarity index 80% rename from grpc/backend.proto rename to grpc_internal.proto index 28fe818..6093f78 100644 --- a/grpc/backend.proto +++ b/grpc_internal.proto @@ -21,18 +21,18 @@ */ syntax = "proto3"; -option go_package = "github.com/strukturag/nextcloud-spreed-signaling/grpc"; +option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling"; -package grpc; +package signaling; -service RpcBackend { - rpc GetSessionCount(GetSessionCountRequest) returns (GetSessionCountReply) {} +service RpcInternal { + rpc GetServerId(GetServerIdRequest) returns (GetServerIdReply) {} } -message GetSessionCountRequest { -string url = 1; +message GetServerIdRequest { } -message GetSessionCountReply { - uint32 count = 1; +message GetServerIdReply { + string serverId = 1; + string version = 2; } diff --git a/grpc/internal_grpc.pb.go b/grpc_internal_grpc.pb.go similarity index 70% rename from grpc/internal_grpc.pb.go rename to grpc_internal_grpc.pb.go index 23d7f42..577ef0d 100644 --- a/grpc/internal_grpc.pb.go +++ b/grpc_internal_grpc.pb.go @@ -20,9 +20,9 @@ // along with this program. If not, see . // Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// source: grpc/internal.proto +// source: grpc_internal.proto -package grpc +package signaling import ( context "context" @@ -37,8 +37,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - RpcInternal_GetServerId_FullMethodName = "/grpc.RpcInternal/GetServerId" - RpcInternal_GetTransientData_FullMethodName = "/grpc.RpcInternal/GetTransientData" + RpcInternal_GetServerId_FullMethodName = "/signaling.RpcInternal/GetServerId" ) // RpcInternalClient is the client API for RpcInternal service. @@ -46,7 +45,6 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type RpcInternalClient interface { GetServerId(ctx context.Context, in *GetServerIdRequest, opts ...grpc.CallOption) (*GetServerIdReply, error) - GetTransientData(ctx context.Context, in *GetTransientDataRequest, opts ...grpc.CallOption) (*GetTransientDataReply, error) } type rpcInternalClient struct { @@ -67,22 +65,11 @@ func (c *rpcInternalClient) GetServerId(ctx context.Context, in *GetServerIdRequ return out, nil } -func (c *rpcInternalClient) GetTransientData(ctx context.Context, in *GetTransientDataRequest, opts ...grpc.CallOption) (*GetTransientDataReply, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetTransientDataReply) - err := c.cc.Invoke(ctx, RpcInternal_GetTransientData_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - // RpcInternalServer is the server API for RpcInternal service. // All implementations must embed UnimplementedRpcInternalServer // for forward compatibility. type RpcInternalServer interface { GetServerId(context.Context, *GetServerIdRequest) (*GetServerIdReply, error) - GetTransientData(context.Context, *GetTransientDataRequest) (*GetTransientDataReply, error) mustEmbedUnimplementedRpcInternalServer() } @@ -94,10 +81,7 @@ type RpcInternalServer interface { type UnimplementedRpcInternalServer struct{} func (UnimplementedRpcInternalServer) GetServerId(context.Context, *GetServerIdRequest) (*GetServerIdReply, error) { - return nil, status.Error(codes.Unimplemented, "method GetServerId not implemented") -} -func (UnimplementedRpcInternalServer) GetTransientData(context.Context, *GetTransientDataRequest) (*GetTransientDataReply, error) { - return nil, status.Error(codes.Unimplemented, "method GetTransientData not implemented") + return nil, status.Errorf(codes.Unimplemented, "method GetServerId not implemented") } func (UnimplementedRpcInternalServer) mustEmbedUnimplementedRpcInternalServer() {} func (UnimplementedRpcInternalServer) testEmbeddedByValue() {} @@ -110,7 +94,7 @@ type UnsafeRpcInternalServer interface { } func RegisterRpcInternalServer(s grpc.ServiceRegistrar, srv RpcInternalServer) { - // If the following call panics, it indicates UnimplementedRpcInternalServer was + // If the following call pancis, it indicates UnimplementedRpcInternalServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -138,40 +122,18 @@ func _RpcInternal_GetServerId_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } -func _RpcInternal_GetTransientData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetTransientDataRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(RpcInternalServer).GetTransientData(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: RpcInternal_GetTransientData_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RpcInternalServer).GetTransientData(ctx, req.(*GetTransientDataRequest)) - } - return interceptor(ctx, in, info, handler) -} - // RpcInternal_ServiceDesc is the grpc.ServiceDesc for RpcInternal service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var RpcInternal_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "grpc.RpcInternal", + ServiceName: "signaling.RpcInternal", HandlerType: (*RpcInternalServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetServerId", Handler: _RpcInternal_GetServerId_Handler, }, - { - MethodName: "GetTransientData", - Handler: _RpcInternal_GetTransientData_Handler, - }, }, Streams: []grpc.StreamDesc{}, - Metadata: "grpc/internal.proto", + Metadata: "grpc_internal.proto", } diff --git a/grpc_mcu.pb.go b/grpc_mcu.pb.go new file mode 100644 index 0000000..59ef5fc --- /dev/null +++ b/grpc_mcu.pb.go @@ -0,0 +1,232 @@ +//* +// Standalone signaling server for the Nextcloud Spreed app. +// Copyright (C) 2022 struktur AG +// +// @author Joachim Bauch +// +// @license GNU AGPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: grpc_mcu.proto + +package signaling + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetPublisherIdRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=sessionId,proto3" json:"sessionId,omitempty"` + StreamType string `protobuf:"bytes,2,opt,name=streamType,proto3" json:"streamType,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPublisherIdRequest) Reset() { + *x = GetPublisherIdRequest{} + mi := &file_grpc_mcu_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPublisherIdRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPublisherIdRequest) ProtoMessage() {} + +func (x *GetPublisherIdRequest) ProtoReflect() protoreflect.Message { + mi := &file_grpc_mcu_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPublisherIdRequest.ProtoReflect.Descriptor instead. +func (*GetPublisherIdRequest) Descriptor() ([]byte, []int) { + return file_grpc_mcu_proto_rawDescGZIP(), []int{0} +} + +func (x *GetPublisherIdRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *GetPublisherIdRequest) GetStreamType() string { + if x != nil { + return x.StreamType + } + return "" +} + +type GetPublisherIdReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + PublisherId string `protobuf:"bytes,1,opt,name=publisherId,proto3" json:"publisherId,omitempty"` + ProxyUrl string `protobuf:"bytes,2,opt,name=proxyUrl,proto3" json:"proxyUrl,omitempty"` + Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPublisherIdReply) Reset() { + *x = GetPublisherIdReply{} + mi := &file_grpc_mcu_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPublisherIdReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPublisherIdReply) ProtoMessage() {} + +func (x *GetPublisherIdReply) ProtoReflect() protoreflect.Message { + mi := &file_grpc_mcu_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPublisherIdReply.ProtoReflect.Descriptor instead. +func (*GetPublisherIdReply) Descriptor() ([]byte, []int) { + return file_grpc_mcu_proto_rawDescGZIP(), []int{1} +} + +func (x *GetPublisherIdReply) GetPublisherId() string { + if x != nil { + return x.PublisherId + } + return "" +} + +func (x *GetPublisherIdReply) GetProxyUrl() string { + if x != nil { + return x.ProxyUrl + } + return "" +} + +func (x *GetPublisherIdReply) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +var File_grpc_mcu_proto protoreflect.FileDescriptor + +var file_grpc_mcu_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x6d, 0x63, 0x75, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x22, 0x55, 0x0a, 0x15, 0x47, + 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x49, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x79, + 0x70, 0x65, 0x22, 0x63, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, + 0x65, 0x72, 0x49, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x55, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x55, 0x72, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x32, 0x5e, 0x0a, 0x06, 0x52, 0x70, 0x63, 0x4d, 0x63, + 0x75, 0x12, 0x54, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, + 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x49, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, + 0x67, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x49, 0x64, + 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x6b, 0x74, 0x75, 0x72, 0x61, 0x67, + 0x2f, 0x6e, 0x65, 0x78, 0x74, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x70, 0x72, 0x65, 0x65, + 0x64, 0x2d, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3b, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_grpc_mcu_proto_rawDescOnce sync.Once + file_grpc_mcu_proto_rawDescData = file_grpc_mcu_proto_rawDesc +) + +func file_grpc_mcu_proto_rawDescGZIP() []byte { + file_grpc_mcu_proto_rawDescOnce.Do(func() { + file_grpc_mcu_proto_rawDescData = protoimpl.X.CompressGZIP(file_grpc_mcu_proto_rawDescData) + }) + return file_grpc_mcu_proto_rawDescData +} + +var file_grpc_mcu_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_grpc_mcu_proto_goTypes = []any{ + (*GetPublisherIdRequest)(nil), // 0: signaling.GetPublisherIdRequest + (*GetPublisherIdReply)(nil), // 1: signaling.GetPublisherIdReply +} +var file_grpc_mcu_proto_depIdxs = []int32{ + 0, // 0: signaling.RpcMcu.GetPublisherId:input_type -> signaling.GetPublisherIdRequest + 1, // 1: signaling.RpcMcu.GetPublisherId:output_type -> signaling.GetPublisherIdReply + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_grpc_mcu_proto_init() } +func file_grpc_mcu_proto_init() { + if File_grpc_mcu_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_grpc_mcu_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_grpc_mcu_proto_goTypes, + DependencyIndexes: file_grpc_mcu_proto_depIdxs, + MessageInfos: file_grpc_mcu_proto_msgTypes, + }.Build() + File_grpc_mcu_proto = out.File + file_grpc_mcu_proto_rawDesc = nil + file_grpc_mcu_proto_goTypes = nil + file_grpc_mcu_proto_depIdxs = nil +} diff --git a/grpc/sfu.proto b/grpc_mcu.proto similarity index 93% rename from grpc/sfu.proto rename to grpc_mcu.proto index e0906c1..b2313d2 100644 --- a/grpc/sfu.proto +++ b/grpc_mcu.proto @@ -21,9 +21,9 @@ */ syntax = "proto3"; -option go_package = "github.com/strukturag/nextcloud-spreed-signaling/grpc"; +option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling"; -package grpc; +package signaling; service RpcMcu { rpc GetPublisherId(GetPublisherIdRequest) returns (GetPublisherIdReply) {} @@ -38,6 +38,4 @@ message GetPublisherIdReply { string publisherId = 1; string proxyUrl = 2; string ip = 3; - string connectToken = 4; - string publisherToken = 5; } diff --git a/grpc/sfu_grpc.pb.go b/grpc_mcu_grpc.pb.go similarity index 93% rename from grpc/sfu_grpc.pb.go rename to grpc_mcu_grpc.pb.go index a1c38dc..3b37591 100644 --- a/grpc/sfu_grpc.pb.go +++ b/grpc_mcu_grpc.pb.go @@ -20,9 +20,9 @@ // along with this program. If not, see . // Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// source: grpc/sfu.proto +// source: grpc_mcu.proto -package grpc +package signaling import ( context "context" @@ -37,7 +37,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - RpcMcu_GetPublisherId_FullMethodName = "/grpc.RpcMcu/GetPublisherId" + RpcMcu_GetPublisherId_FullMethodName = "/signaling.RpcMcu/GetPublisherId" ) // RpcMcuClient is the client API for RpcMcu service. @@ -81,7 +81,7 @@ type RpcMcuServer interface { type UnimplementedRpcMcuServer struct{} func (UnimplementedRpcMcuServer) GetPublisherId(context.Context, *GetPublisherIdRequest) (*GetPublisherIdReply, error) { - return nil, status.Error(codes.Unimplemented, "method GetPublisherId not implemented") + return nil, status.Errorf(codes.Unimplemented, "method GetPublisherId not implemented") } func (UnimplementedRpcMcuServer) mustEmbedUnimplementedRpcMcuServer() {} func (UnimplementedRpcMcuServer) testEmbeddedByValue() {} @@ -94,7 +94,7 @@ type UnsafeRpcMcuServer interface { } func RegisterRpcMcuServer(s grpc.ServiceRegistrar, srv RpcMcuServer) { - // If the following call panics, it indicates UnimplementedRpcMcuServer was + // If the following call pancis, it indicates UnimplementedRpcMcuServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -126,7 +126,7 @@ func _RpcMcu_GetPublisherId_Handler(srv interface{}, ctx context.Context, dec fu // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var RpcMcu_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "grpc.RpcMcu", + ServiceName: "signaling.RpcMcu", HandlerType: (*RpcMcuServer)(nil), Methods: []grpc.MethodDesc{ { @@ -135,5 +135,5 @@ var RpcMcu_ServiceDesc = grpc.ServiceDesc{ }, }, Streams: []grpc.StreamDesc{}, - Metadata: "grpc/sfu.proto", + Metadata: "grpc_mcu.proto", } diff --git a/server/grpc_remote_client.go b/grpc_remote_client.go similarity index 68% rename from server/grpc_remote_client.go rename to grpc_remote_client.go index a81fdd2..8940fde 100644 --- a/server/grpc_remote_client.go +++ b/grpc_remote_client.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" @@ -27,17 +27,12 @@ import ( "errors" "fmt" "io" + "log" "sync/atomic" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/client" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) const ( @@ -54,23 +49,22 @@ func getMD(md metadata.MD, key string) string { // remoteGrpcClient is a remote client connecting from a GRPC proxy to a Hub. type remoteGrpcClient struct { - logger log.Logger hub *Hub - client grpc.RpcSessions_ProxySessionServer + client RpcSessions_ProxySessionServer - sessionId api.PublicSessionId + sessionId string remoteAddr string - country geoip.Country + country string userAgent string closeCtx context.Context closeFunc context.CancelCauseFunc session atomic.Pointer[Session] - messages chan client.WritableClientMessage + messages chan WritableClientMessage } -func newRemoteGrpcClient(hub *Hub, request grpc.RpcSessions_ProxySessionServer) (*remoteGrpcClient, error) { +func newRemoteGrpcClient(hub *Hub, request RpcSessions_ProxySessionServer) (*remoteGrpcClient, error) { md, found := metadata.FromIncomingContext(request.Context()) if !found { return nil, errors.New("no metadata provided") @@ -79,19 +73,18 @@ func newRemoteGrpcClient(hub *Hub, request grpc.RpcSessions_ProxySessionServer) closeCtx, closeFunc := context.WithCancelCause(context.Background()) result := &remoteGrpcClient{ - logger: hub.logger, hub: hub, client: request, - sessionId: api.PublicSessionId(getMD(md, "sessionId")), + sessionId: getMD(md, "sessionId"), remoteAddr: getMD(md, "remoteAddr"), - country: geoip.Country(getMD(md, "country")), + country: getMD(md, "country"), userAgent: getMD(md, "userAgent"), closeCtx: closeCtx, closeFunc: closeFunc, - messages: make(chan client.WritableClientMessage, grpcRemoteClientMessageQueue), + messages: make(chan WritableClientMessage, grpcRemoteClientMessageQueue), } return result, nil } @@ -100,7 +93,7 @@ func (c *remoteGrpcClient) readPump() { var closeError error defer func() { c.closeFunc(closeError) - c.hub.processUnregister(c) + c.hub.OnClosed(c) }() for { @@ -112,13 +105,13 @@ func (c *remoteGrpcClient) readPump() { } if status.Code(err) != codes.Canceled { - c.logger.Printf("Error reading from remote client for session %s: %s", c.sessionId, err) + log.Printf("Error reading from remote client for session %s: %s", c.sessionId, err) closeError = err } break } - c.hub.processMessage(c, msg.Message) + c.hub.OnMessageReceived(c, msg.Message) } } @@ -134,7 +127,7 @@ func (c *remoteGrpcClient) UserAgent() string { return c.userAgent } -func (c *remoteGrpcClient) Country() geoip.Country { +func (c *remoteGrpcClient) Country() string { return c.country } @@ -146,10 +139,6 @@ func (c *remoteGrpcClient) IsAuthenticated() bool { return c.GetSession() != nil } -func (c *remoteGrpcClient) GetSessionId() api.PublicSessionId { - return c.sessionId -} - func (c *remoteGrpcClient) GetSession() Session { session := c.session.Load() if session == nil { @@ -167,20 +156,20 @@ func (c *remoteGrpcClient) SetSession(session Session) { } } -func (c *remoteGrpcClient) SendError(e *api.Error) bool { - message := &api.ServerMessage{ +func (c *remoteGrpcClient) SendError(e *Error) bool { + message := &ServerMessage{ Type: "error", Error: e, } return c.SendMessage(message) } -func (c *remoteGrpcClient) SendByeResponse(message *api.ClientMessage) bool { +func (c *remoteGrpcClient) SendByeResponse(message *ClientMessage) bool { return c.SendByeResponseWithReason(message, "") } -func (c *remoteGrpcClient) SendByeResponseWithReason(message *api.ClientMessage, reason string) bool { - response := &api.ServerMessage{ +func (c *remoteGrpcClient) SendByeResponseWithReason(message *ClientMessage, reason string) bool { + response := &ServerMessage{ Type: "bye", } if message != nil { @@ -188,14 +177,14 @@ func (c *remoteGrpcClient) SendByeResponseWithReason(message *api.ClientMessage, } if reason != "" { if response.Bye == nil { - response.Bye = &api.ByeServerMessage{} + response.Bye = &ByeServerMessage{} } response.Bye.Reason = reason } return c.SendMessage(response) } -func (c *remoteGrpcClient) SendMessage(message client.WritableClientMessage) bool { +func (c *remoteGrpcClient) SendMessage(message WritableClientMessage) bool { if c.closeCtx.Err() != nil { return false } @@ -204,7 +193,7 @@ func (c *remoteGrpcClient) SendMessage(message client.WritableClientMessage) boo case c.messages <- message: return true default: - c.logger.Printf("Message queue for remote client of session %s is full, not sending %+v", c.sessionId, message) + log.Printf("Message queue for remote client of session %s is full, not sending %+v", c.sessionId, message) return false } } @@ -226,11 +215,11 @@ func (c *remoteGrpcClient) run() error { case msg := <-c.messages: data, err := json.Marshal(msg) if err != nil { - c.logger.Printf("Error marshalling %+v for remote client for session %s: %s", msg, c.sessionId, err) + log.Printf("Error marshalling %+v for remote client for session %s: %s", msg, c.sessionId, err) continue } - if err := c.client.Send(&grpc.ServerSessionMessage{ + if err := c.client.Send(&ServerSessionMessage{ Message: data, }); err != nil { return fmt.Errorf("error sending %+v to remote client for session %s: %w", msg, c.sessionId, err) diff --git a/grpc_server.go b/grpc_server.go new file mode 100644 index 0000000..77c6aec --- /dev/null +++ b/grpc_server.go @@ -0,0 +1,308 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "log" + "net" + "net/url" + "os" + + "github.com/dlintw/goconf" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + status "google.golang.org/grpc/status" +) + +var ( + GrpcServerId string +) + +func init() { + RegisterGrpcServerStats() + + hostname, err := os.Hostname() + if err != nil { + hostname = newRandomString(8) + } + md := sha256.New() + md.Write([]byte(fmt.Sprintf("%s-%s-%d", newRandomString(32), hostname, os.Getpid()))) + GrpcServerId = hex.EncodeToString(md.Sum(nil)) +} + +type GrpcServerHub interface { + GetSessionByResumeId(resumeId string) Session + GetSessionByPublicId(sessionId string) Session + GetSessionIdByRoomSessionId(roomSessionId string) (string, error) + GetRoomForBackend(roomId string, backend *Backend) *Room + + GetBackend(u *url.URL) *Backend +} + +type GrpcServer struct { + UnimplementedRpcBackendServer + UnimplementedRpcInternalServer + UnimplementedRpcMcuServer + UnimplementedRpcSessionsServer + + version string + creds credentials.TransportCredentials + conn *grpc.Server + listener net.Listener + serverId string // can be overwritten from tests + + hub GrpcServerHub +} + +func NewGrpcServer(config *goconf.ConfigFile, version string) (*GrpcServer, error) { + var listener net.Listener + if addr, _ := GetStringOptionWithEnv(config, "grpc", "listen"); addr != "" { + var err error + listener, err = net.Listen("tcp", addr) + if err != nil { + return nil, fmt.Errorf("could not create GRPC listener %s: %w", addr, err) + } + } + + creds, err := NewReloadableCredentials(config, true) + if err != nil { + return nil, err + } + + conn := grpc.NewServer(grpc.Creds(creds)) + result := &GrpcServer{ + version: version, + creds: creds, + conn: conn, + listener: listener, + serverId: GrpcServerId, + } + RegisterRpcBackendServer(conn, result) + RegisterRpcInternalServer(conn, result) + RegisterRpcSessionsServer(conn, result) + RegisterRpcMcuServer(conn, result) + return result, nil +} + +func (s *GrpcServer) Run() error { + if s.listener == nil { + return nil + } + + return s.conn.Serve(s.listener) +} + +func (s *GrpcServer) Close() { + s.conn.GracefulStop() + if cr, ok := s.creds.(*reloadableCredentials); ok { + cr.Close() + } +} + +func (s *GrpcServer) LookupResumeId(ctx context.Context, request *LookupResumeIdRequest) (*LookupResumeIdReply, error) { + statsGrpcServerCalls.WithLabelValues("LookupResumeId").Inc() + // TODO: Remove debug logging + log.Printf("Lookup session for resume id %s", request.ResumeId) + session := s.hub.GetSessionByResumeId(request.ResumeId) + if session == nil { + return nil, status.Error(codes.NotFound, "no such room session id") + } + + return &LookupResumeIdReply{ + SessionId: session.PublicId(), + }, nil +} + +func (s *GrpcServer) LookupSessionId(ctx context.Context, request *LookupSessionIdRequest) (*LookupSessionIdReply, error) { + statsGrpcServerCalls.WithLabelValues("LookupSessionId").Inc() + // TODO: Remove debug logging + log.Printf("Lookup session id for room session id %s", request.RoomSessionId) + sid, err := s.hub.GetSessionIdByRoomSessionId(request.RoomSessionId) + if errors.Is(err, ErrNoSuchRoomSession) { + return nil, status.Error(codes.NotFound, "no such room session id") + } else if err != nil { + return nil, err + } + + if sid != "" && request.DisconnectReason != "" { + if session := s.hub.GetSessionByPublicId(sid); session != nil { + log.Printf("Closing session %s because same room session %s connected", session.PublicId(), request.RoomSessionId) + session.LeaveRoom(false) + switch sess := session.(type) { + case *ClientSession: + if client := sess.GetClient(); client != nil { + client.SendByeResponseWithReason(nil, "room_session_reconnected") + } + } + session.Close() + } + } + return &LookupSessionIdReply{ + SessionId: sid, + }, nil +} + +func (s *GrpcServer) IsSessionInCall(ctx context.Context, request *IsSessionInCallRequest) (*IsSessionInCallReply, error) { + statsGrpcServerCalls.WithLabelValues("IsSessionInCall").Inc() + // TODO: Remove debug logging + log.Printf("Check if session %s is in call %s on %s", request.SessionId, request.RoomId, request.BackendUrl) + session := s.hub.GetSessionByPublicId(request.SessionId) + if session == nil { + return nil, status.Error(codes.NotFound, "no such session id") + } + + result := &IsSessionInCallReply{} + room := session.GetRoom() + if room == nil || room.Id() != request.GetRoomId() || room.Backend().url != request.GetBackendUrl() || + (session.ClientType() != HelloClientTypeInternal && !room.IsSessionInCall(session)) { + // Recipient is not in a room, a different room or not in the call. + result.InCall = false + } else { + result.InCall = true + } + return result, nil +} + +func (s *GrpcServer) GetInternalSessions(ctx context.Context, request *GetInternalSessionsRequest) (*GetInternalSessionsReply, error) { + statsGrpcServerCalls.WithLabelValues("GetInternalSessions").Inc() + // TODO: Remove debug logging + log.Printf("Get internal sessions from %s on %s", request.RoomId, request.BackendUrl) + + var u *url.URL + if request.BackendUrl != "" { + var err error + u, err = url.Parse(request.BackendUrl) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid url") + } + } + + backend := s.hub.GetBackend(u) + if backend == nil { + return nil, status.Error(codes.NotFound, "no such backend") + } + + room := s.hub.GetRoomForBackend(request.RoomId, backend) + if room == nil { + return nil, status.Error(codes.NotFound, "no such room") + } + + result := &GetInternalSessionsReply{} + room.mu.RLock() + defer room.mu.RUnlock() + + for session := range room.internalSessions { + result.InternalSessions = append(result.InternalSessions, &InternalSessionData{ + SessionId: session.PublicId(), + InCall: uint32(session.GetInCall()), + Features: session.GetFeatures(), + }) + } + + for session := range room.virtualSessions { + result.VirtualSessions = append(result.VirtualSessions, &VirtualSessionData{ + SessionId: session.PublicId(), + InCall: uint32(session.GetInCall()), + }) + } + + return result, nil +} + +func (s *GrpcServer) GetPublisherId(ctx context.Context, request *GetPublisherIdRequest) (*GetPublisherIdReply, error) { + statsGrpcServerCalls.WithLabelValues("GetPublisherId").Inc() + // TODO: Remove debug logging + log.Printf("Get %s publisher id for session %s", request.StreamType, request.SessionId) + session := s.hub.GetSessionByPublicId(request.SessionId) + if session == nil { + return nil, status.Error(codes.NotFound, "no such session") + } + + clientSession, ok := session.(*ClientSession) + if !ok { + return nil, status.Error(codes.NotFound, "no such session") + } + + publisher := clientSession.GetOrWaitForPublisher(ctx, StreamType(request.StreamType)) + if publisher, ok := publisher.(*mcuProxyPublisher); ok { + reply := &GetPublisherIdReply{ + PublisherId: publisher.Id(), + ProxyUrl: publisher.conn.rawUrl, + } + if ip := publisher.conn.ip; ip != nil { + reply.Ip = ip.String() + } + return reply, nil + } + + return nil, status.Error(codes.NotFound, "no such publisher") +} + +func (s *GrpcServer) GetServerId(ctx context.Context, request *GetServerIdRequest) (*GetServerIdReply, error) { + statsGrpcServerCalls.WithLabelValues("GetServerId").Inc() + return &GetServerIdReply{ + ServerId: s.serverId, + Version: s.version, + }, nil +} + +func (s *GrpcServer) GetSessionCount(ctx context.Context, request *GetSessionCountRequest) (*GetSessionCountReply, error) { + statsGrpcServerCalls.WithLabelValues("SessionCount").Inc() + + u, err := url.Parse(request.Url) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid url") + } + + backend := s.hub.GetBackend(u) + if backend == nil { + return nil, status.Error(codes.NotFound, "no such backend") + } + + return &GetSessionCountReply{ + Count: uint32(backend.Len()), + }, nil +} + +func (s *GrpcServer) ProxySession(request RpcSessions_ProxySessionServer) error { + statsGrpcServerCalls.WithLabelValues("ProxySession").Inc() + hub, ok := s.hub.(*Hub) + if !ok { + return status.Error(codes.Internal, "invalid hub type") + + } + client, err := newRemoteGrpcClient(hub, request) + if err != nil { + return err + } + + sid := hub.registerClient(client) + defer hub.unregisterClient(sid) + + return client.run() +} diff --git a/grpc_server_test.go b/grpc_server_test.go new file mode 100644 index 0000000..e985fd2 --- /dev/null +++ b/grpc_server_test.go @@ -0,0 +1,250 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "net" + "os" + "path" + "strconv" + "testing" + "time" + + "github.com/dlintw/goconf" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func (s *GrpcServer) WaitForCertificateReload(ctx context.Context) error { + c, ok := s.creds.(*reloadableCredentials) + if !ok { + return errors.New("no reloadable credentials found") + } + + return c.WaitForCertificateReload(ctx) +} + +func (s *GrpcServer) WaitForCertPoolReload(ctx context.Context) error { + c, ok := s.creds.(*reloadableCredentials) + if !ok { + return errors.New("no reloadable credentials found") + } + + return c.WaitForCertPoolReload(ctx) +} + +func NewGrpcServerForTestWithConfig(t *testing.T, config *goconf.ConfigFile) (server *GrpcServer, addr string) { + for port := 50000; port < 50100; port++ { + addr = net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) + config.AddOption("grpc", "listen", addr) + var err error + server, err = NewGrpcServer(config, "0.0.0") + if isErrorAddressAlreadyInUse(err) { + continue + } + + require.NoError(t, err) + break + } + + require.NotNil(t, server, "could not find free port") + + // Don't match with own server id by default. + server.serverId = "dont-match" + + go func() { + assert.NoError(t, server.Run(), "could not start GRPC server") + }() + + t.Cleanup(func() { + server.Close() + }) + return server, addr +} + +func NewGrpcServerForTest(t *testing.T) (server *GrpcServer, addr string) { + config := goconf.NewConfigFile() + return NewGrpcServerForTestWithConfig(t, config) +} + +func Test_GrpcServer_ReloadCerts(t *testing.T) { + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + key, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(err) + + org1 := "Testing certificate" + cert1 := GenerateSelfSignedCertificateForTesting(t, 1024, org1, key) + + dir := t.TempDir() + privkeyFile := path.Join(dir, "privkey.pem") + pubkeyFile := path.Join(dir, "pubkey.pem") + certFile := path.Join(dir, "cert.pem") + WritePrivateKey(key, privkeyFile) // nolint + WritePublicKey(&key.PublicKey, pubkeyFile) // nolint + os.WriteFile(certFile, cert1, 0755) // nolint + + config := goconf.NewConfigFile() + config.AddOption("grpc", "servercertificate", certFile) + config.AddOption("grpc", "serverkey", privkeyFile) + + UpdateCertificateCheckIntervalForTest(t, 0) + server, addr := NewGrpcServerForTestWithConfig(t, config) + + cp1 := x509.NewCertPool() + if !cp1.AppendCertsFromPEM(cert1) { + require.Fail("could not add certificate") + } + + cfg1 := &tls.Config{ + RootCAs: cp1, + } + conn1, err := tls.Dial("tcp", addr, cfg1) + require.NoError(err) + defer conn1.Close() // nolint + state1 := conn1.ConnectionState() + if certs := state1.PeerCertificates; assert.NotEmpty(certs) { + if assert.NotEmpty(certs[0].Subject.Organization) { + assert.Equal(org1, certs[0].Subject.Organization[0]) + } + } + + org2 := "Updated certificate" + cert2 := GenerateSelfSignedCertificateForTesting(t, 1024, org2, key) + replaceFile(t, certFile, cert2, 0755) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + require.NoError(server.WaitForCertificateReload(ctx)) + + cp2 := x509.NewCertPool() + if !cp2.AppendCertsFromPEM(cert2) { + require.Fail("could not add certificate") + } + + cfg2 := &tls.Config{ + RootCAs: cp2, + } + conn2, err := tls.Dial("tcp", addr, cfg2) + require.NoError(err) + defer conn2.Close() // nolint + state2 := conn2.ConnectionState() + if certs := state2.PeerCertificates; assert.NotEmpty(certs) { + if assert.NotEmpty(certs[0].Subject.Organization) { + assert.Equal(org2, certs[0].Subject.Organization[0]) + } + } +} + +func Test_GrpcServer_ReloadCA(t *testing.T) { + CatchLogForTest(t) + require := require.New(t) + serverKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(err) + clientKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(err) + + serverCert := GenerateSelfSignedCertificateForTesting(t, 1024, "Server cert", serverKey) + org1 := "Testing client" + clientCert1 := GenerateSelfSignedCertificateForTesting(t, 1024, org1, clientKey) + + dir := t.TempDir() + privkeyFile := path.Join(dir, "privkey.pem") + pubkeyFile := path.Join(dir, "pubkey.pem") + certFile := path.Join(dir, "cert.pem") + caFile := path.Join(dir, "ca.pem") + WritePrivateKey(serverKey, privkeyFile) // nolint + WritePublicKey(&serverKey.PublicKey, pubkeyFile) // nolint + os.WriteFile(certFile, serverCert, 0755) // nolint + os.WriteFile(caFile, clientCert1, 0755) // nolint + + config := goconf.NewConfigFile() + config.AddOption("grpc", "servercertificate", certFile) + config.AddOption("grpc", "serverkey", privkeyFile) + config.AddOption("grpc", "clientca", caFile) + + UpdateCertificateCheckIntervalForTest(t, 0) + server, addr := NewGrpcServerForTestWithConfig(t, config) + + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(serverCert) { + require.Fail("could not add certificate") + } + + pair1, err := tls.X509KeyPair(clientCert1, pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(clientKey), + })) + require.NoError(err) + + cfg1 := &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{pair1}, + } + client1, err := NewGrpcClient(addr, nil, grpc.WithTransportCredentials(credentials.NewTLS(cfg1))) + require.NoError(err) + defer client1.Close() // nolint + + ctx1, cancel1 := context.WithTimeout(context.Background(), time.Second) + defer cancel1() + + _, _, err = client1.GetServerId(ctx1) + require.NoError(err) + + org2 := "Updated client" + clientCert2 := GenerateSelfSignedCertificateForTesting(t, 1024, org2, clientKey) + replaceFile(t, caFile, clientCert2, 0755) + + require.NoError(server.WaitForCertPoolReload(ctx1)) + + pair2, err := tls.X509KeyPair(clientCert2, pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(clientKey), + })) + require.NoError(err) + + cfg2 := &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{pair2}, + } + client2, err := NewGrpcClient(addr, nil, grpc.WithTransportCredentials(credentials.NewTLS(cfg2))) + require.NoError(err) + defer client2.Close() // nolint + + ctx2, cancel2 := context.WithTimeout(context.Background(), time.Second) + defer cancel2() + + // This will fail if the CA certificate has not been reloaded by the server. + _, _, err = client2.GetServerId(ctx2) + require.NoError(err) +} diff --git a/grpc/sessions.pb.go b/grpc_sessions.pb.go similarity index 61% rename from grpc/sessions.pb.go rename to grpc_sessions.pb.go index 783d885..f47e8fb 100644 --- a/grpc/sessions.pb.go +++ b/grpc_sessions.pb.go @@ -20,16 +20,15 @@ // along with this program. If not, see . // Code generated by protoc-gen-go. DO NOT EDIT. -// source: grpc/sessions.proto +// source: grpc_sessions.proto -package grpc +package signaling import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" - unsafe "unsafe" ) const ( @@ -329,11 +328,9 @@ func (x *IsSessionInCallReply) GetInCall() bool { } type GetInternalSessionsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - RoomId string `protobuf:"bytes,1,opt,name=roomId,proto3" json:"roomId,omitempty"` - // Deprecated: Marked as deprecated in grpc/sessions.proto. - BackendUrl string `protobuf:"bytes,2,opt,name=backendUrl,proto3" json:"backendUrl,omitempty"` - BackendUrls []string `protobuf:"bytes,3,rep,name=backendUrls,proto3" json:"backendUrls,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RoomId string `protobuf:"bytes,1,opt,name=roomId,proto3" json:"roomId,omitempty"` + BackendUrl string `protobuf:"bytes,2,opt,name=backendUrl,proto3" json:"backendUrl,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -375,7 +372,6 @@ func (x *GetInternalSessionsRequest) GetRoomId() string { return "" } -// Deprecated: Marked as deprecated in grpc/sessions.proto. func (x *GetInternalSessionsRequest) GetBackendUrl() string { if x != nil { return x.BackendUrl @@ -383,13 +379,6 @@ func (x *GetInternalSessionsRequest) GetBackendUrl() string { return "" } -func (x *GetInternalSessionsRequest) GetBackendUrls() []string { - if x != nil { - return x.BackendUrls - } - return nil -} - type InternalSessionData struct { state protoimpl.MessageState `protogen:"open.v1"` SessionId string `protobuf:"bytes,1,opt,name=sessionId,proto3" json:"sessionId,omitempty"` @@ -644,93 +633,146 @@ func (x *ServerSessionMessage) GetMessage() []byte { var File_grpc_sessions_proto protoreflect.FileDescriptor -const file_grpc_sessions_proto_rawDesc = "" + - "\n" + - "\x13grpc/sessions.proto\x12\x04grpc\"3\n" + - "\x15LookupResumeIdRequest\x12\x1a\n" + - "\bresumeId\x18\x01 \x01(\tR\bresumeId\"3\n" + - "\x13LookupResumeIdReply\x12\x1c\n" + - "\tsessionId\x18\x01 \x01(\tR\tsessionId\"j\n" + - "\x16LookupSessionIdRequest\x12$\n" + - "\rroomSessionId\x18\x01 \x01(\tR\rroomSessionId\x12*\n" + - "\x10disconnectReason\x18\x02 \x01(\tR\x10disconnectReason\"4\n" + - "\x14LookupSessionIdReply\x12\x1c\n" + - "\tsessionId\x18\x01 \x01(\tR\tsessionId\"n\n" + - "\x16IsSessionInCallRequest\x12\x1c\n" + - "\tsessionId\x18\x01 \x01(\tR\tsessionId\x12\x16\n" + - "\x06roomId\x18\x02 \x01(\tR\x06roomId\x12\x1e\n" + - "\n" + - "backendUrl\x18\x03 \x01(\tR\n" + - "backendUrl\".\n" + - "\x14IsSessionInCallReply\x12\x16\n" + - "\x06inCall\x18\x01 \x01(\bR\x06inCall\"z\n" + - "\x1aGetInternalSessionsRequest\x12\x16\n" + - "\x06roomId\x18\x01 \x01(\tR\x06roomId\x12\"\n" + - "\n" + - "backendUrl\x18\x02 \x01(\tB\x02\x18\x01R\n" + - "backendUrl\x12 \n" + - "\vbackendUrls\x18\x03 \x03(\tR\vbackendUrls\"g\n" + - "\x13InternalSessionData\x12\x1c\n" + - "\tsessionId\x18\x01 \x01(\tR\tsessionId\x12\x16\n" + - "\x06inCall\x18\x02 \x01(\rR\x06inCall\x12\x1a\n" + - "\bfeatures\x18\x03 \x03(\tR\bfeatures\"J\n" + - "\x12VirtualSessionData\x12\x1c\n" + - "\tsessionId\x18\x01 \x01(\tR\tsessionId\x12\x16\n" + - "\x06inCall\x18\x02 \x01(\rR\x06inCall\"\xa5\x01\n" + - "\x18GetInternalSessionsReply\x12E\n" + - "\x10internalSessions\x18\x01 \x03(\v2\x19.grpc.InternalSessionDataR\x10internalSessions\x12B\n" + - "\x0fvirtualSessions\x18\x02 \x03(\v2\x18.grpc.VirtualSessionDataR\x0fvirtualSessions\"0\n" + - "\x14ClientSessionMessage\x12\x18\n" + - "\amessage\x18\x01 \x01(\fR\amessage\"0\n" + - "\x14ServerSessionMessage\x12\x18\n" + - "\amessage\x18\x01 \x01(\fR\amessage2\xa0\x03\n" + - "\vRpcSessions\x12J\n" + - "\x0eLookupResumeId\x12\x1b.grpc.LookupResumeIdRequest\x1a\x19.grpc.LookupResumeIdReply\"\x00\x12M\n" + - "\x0fLookupSessionId\x12\x1c.grpc.LookupSessionIdRequest\x1a\x1a.grpc.LookupSessionIdReply\"\x00\x12M\n" + - "\x0fIsSessionInCall\x12\x1c.grpc.IsSessionInCallRequest\x1a\x1a.grpc.IsSessionInCallReply\"\x00\x12Y\n" + - "\x13GetInternalSessions\x12 .grpc.GetInternalSessionsRequest\x1a\x1e.grpc.GetInternalSessionsReply\"\x00\x12L\n" + - "\fProxySession\x12\x1a.grpc.ClientSessionMessage\x1a\x1a.grpc.ServerSessionMessage\"\x00(\x010\x01B7Z5github.com/strukturag/nextcloud-spreed-signaling/grpcb\x06proto3" +var file_grpc_sessions_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, + 0x22, 0x33, 0x0a, 0x15, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, + 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, + 0x75, 0x6d, 0x65, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x75, 0x6d, 0x65, 0x49, 0x64, 0x22, 0x33, 0x0a, 0x13, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, + 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1c, 0x0a, 0x09, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x6a, 0x0a, 0x16, 0x4c, 0x6f, + 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x6f, 0x6f, 0x6d, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x6f, 0x6f, + 0x6d, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x64, 0x69, + 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x34, 0x0a, 0x14, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1c, + 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x6e, 0x0a, 0x16, + 0x49, 0x73, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, + 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x55, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x55, 0x72, 0x6c, 0x22, 0x2e, 0x0a, 0x14, + 0x49, 0x73, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x22, 0x54, 0x0a, 0x1a, + 0x47, 0x65, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, + 0x6f, 0x6d, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, + 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x55, 0x72, 0x6c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x55, + 0x72, 0x6c, 0x22, 0x67, 0x0a, 0x13, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x43, 0x61, 0x6c, + 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x69, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x12, + 0x1a, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x22, 0x4a, 0x0a, 0x12, 0x56, + 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, + 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x06, 0x69, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x22, 0xaf, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x49, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x12, 0x4a, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, + 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x10, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x47, 0x0a, 0x0f, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x56, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, + 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x30, 0x0a, 0x14, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x30, 0x0a, 0x14, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xd2, 0x03, + 0x0a, 0x0b, 0x52, 0x70, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x54, 0x0a, + 0x0e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x64, 0x12, + 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, + 0x75, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1e, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x6f, + 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x64, 0x52, 0x65, 0x70, 0x6c, + 0x79, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x0f, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x21, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, + 0x6e, 0x67, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x49, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x0f, + 0x49, 0x73, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x12, + 0x21, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x49, 0x73, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x49, + 0x73, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, + 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x63, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x73, + 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, + 0x47, 0x65, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x0c, 0x50, 0x72, + 0x6f, 0x78, 0x79, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1f, 0x2e, 0x73, 0x69, + 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, + 0x30, 0x01, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x73, 0x74, 0x72, 0x75, 0x6b, 0x74, 0x75, 0x72, 0x61, 0x67, 0x2f, 0x6e, 0x65, 0x78, 0x74, + 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x70, 0x72, 0x65, 0x65, 0x64, 0x2d, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} var ( file_grpc_sessions_proto_rawDescOnce sync.Once - file_grpc_sessions_proto_rawDescData []byte + file_grpc_sessions_proto_rawDescData = file_grpc_sessions_proto_rawDesc ) func file_grpc_sessions_proto_rawDescGZIP() []byte { file_grpc_sessions_proto_rawDescOnce.Do(func() { - file_grpc_sessions_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_grpc_sessions_proto_rawDesc), len(file_grpc_sessions_proto_rawDesc))) + file_grpc_sessions_proto_rawDescData = protoimpl.X.CompressGZIP(file_grpc_sessions_proto_rawDescData) }) return file_grpc_sessions_proto_rawDescData } var file_grpc_sessions_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_grpc_sessions_proto_goTypes = []any{ - (*LookupResumeIdRequest)(nil), // 0: grpc.LookupResumeIdRequest - (*LookupResumeIdReply)(nil), // 1: grpc.LookupResumeIdReply - (*LookupSessionIdRequest)(nil), // 2: grpc.LookupSessionIdRequest - (*LookupSessionIdReply)(nil), // 3: grpc.LookupSessionIdReply - (*IsSessionInCallRequest)(nil), // 4: grpc.IsSessionInCallRequest - (*IsSessionInCallReply)(nil), // 5: grpc.IsSessionInCallReply - (*GetInternalSessionsRequest)(nil), // 6: grpc.GetInternalSessionsRequest - (*InternalSessionData)(nil), // 7: grpc.InternalSessionData - (*VirtualSessionData)(nil), // 8: grpc.VirtualSessionData - (*GetInternalSessionsReply)(nil), // 9: grpc.GetInternalSessionsReply - (*ClientSessionMessage)(nil), // 10: grpc.ClientSessionMessage - (*ServerSessionMessage)(nil), // 11: grpc.ServerSessionMessage + (*LookupResumeIdRequest)(nil), // 0: signaling.LookupResumeIdRequest + (*LookupResumeIdReply)(nil), // 1: signaling.LookupResumeIdReply + (*LookupSessionIdRequest)(nil), // 2: signaling.LookupSessionIdRequest + (*LookupSessionIdReply)(nil), // 3: signaling.LookupSessionIdReply + (*IsSessionInCallRequest)(nil), // 4: signaling.IsSessionInCallRequest + (*IsSessionInCallReply)(nil), // 5: signaling.IsSessionInCallReply + (*GetInternalSessionsRequest)(nil), // 6: signaling.GetInternalSessionsRequest + (*InternalSessionData)(nil), // 7: signaling.InternalSessionData + (*VirtualSessionData)(nil), // 8: signaling.VirtualSessionData + (*GetInternalSessionsReply)(nil), // 9: signaling.GetInternalSessionsReply + (*ClientSessionMessage)(nil), // 10: signaling.ClientSessionMessage + (*ServerSessionMessage)(nil), // 11: signaling.ServerSessionMessage } var file_grpc_sessions_proto_depIdxs = []int32{ - 7, // 0: grpc.GetInternalSessionsReply.internalSessions:type_name -> grpc.InternalSessionData - 8, // 1: grpc.GetInternalSessionsReply.virtualSessions:type_name -> grpc.VirtualSessionData - 0, // 2: grpc.RpcSessions.LookupResumeId:input_type -> grpc.LookupResumeIdRequest - 2, // 3: grpc.RpcSessions.LookupSessionId:input_type -> grpc.LookupSessionIdRequest - 4, // 4: grpc.RpcSessions.IsSessionInCall:input_type -> grpc.IsSessionInCallRequest - 6, // 5: grpc.RpcSessions.GetInternalSessions:input_type -> grpc.GetInternalSessionsRequest - 10, // 6: grpc.RpcSessions.ProxySession:input_type -> grpc.ClientSessionMessage - 1, // 7: grpc.RpcSessions.LookupResumeId:output_type -> grpc.LookupResumeIdReply - 3, // 8: grpc.RpcSessions.LookupSessionId:output_type -> grpc.LookupSessionIdReply - 5, // 9: grpc.RpcSessions.IsSessionInCall:output_type -> grpc.IsSessionInCallReply - 9, // 10: grpc.RpcSessions.GetInternalSessions:output_type -> grpc.GetInternalSessionsReply - 11, // 11: grpc.RpcSessions.ProxySession:output_type -> grpc.ServerSessionMessage + 7, // 0: signaling.GetInternalSessionsReply.internalSessions:type_name -> signaling.InternalSessionData + 8, // 1: signaling.GetInternalSessionsReply.virtualSessions:type_name -> signaling.VirtualSessionData + 0, // 2: signaling.RpcSessions.LookupResumeId:input_type -> signaling.LookupResumeIdRequest + 2, // 3: signaling.RpcSessions.LookupSessionId:input_type -> signaling.LookupSessionIdRequest + 4, // 4: signaling.RpcSessions.IsSessionInCall:input_type -> signaling.IsSessionInCallRequest + 6, // 5: signaling.RpcSessions.GetInternalSessions:input_type -> signaling.GetInternalSessionsRequest + 10, // 6: signaling.RpcSessions.ProxySession:input_type -> signaling.ClientSessionMessage + 1, // 7: signaling.RpcSessions.LookupResumeId:output_type -> signaling.LookupResumeIdReply + 3, // 8: signaling.RpcSessions.LookupSessionId:output_type -> signaling.LookupSessionIdReply + 5, // 9: signaling.RpcSessions.IsSessionInCall:output_type -> signaling.IsSessionInCallReply + 9, // 10: signaling.RpcSessions.GetInternalSessions:output_type -> signaling.GetInternalSessionsReply + 11, // 11: signaling.RpcSessions.ProxySession:output_type -> signaling.ServerSessionMessage 7, // [7:12] is the sub-list for method output_type 2, // [2:7] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name @@ -747,7 +789,7 @@ func file_grpc_sessions_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_grpc_sessions_proto_rawDesc), len(file_grpc_sessions_proto_rawDesc)), + RawDescriptor: file_grpc_sessions_proto_rawDesc, NumEnums: 0, NumMessages: 12, NumExtensions: 0, @@ -758,6 +800,7 @@ func file_grpc_sessions_proto_init() { MessageInfos: file_grpc_sessions_proto_msgTypes, }.Build() File_grpc_sessions_proto = out.File + file_grpc_sessions_proto_rawDesc = nil file_grpc_sessions_proto_goTypes = nil file_grpc_sessions_proto_depIdxs = nil } diff --git a/grpc/sessions.proto b/grpc_sessions.proto similarity index 95% rename from grpc/sessions.proto rename to grpc_sessions.proto index 858eca0..3d29dfe 100644 --- a/grpc/sessions.proto +++ b/grpc_sessions.proto @@ -21,9 +21,9 @@ */ syntax = "proto3"; -option go_package = "github.com/strukturag/nextcloud-spreed-signaling/grpc"; +option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling"; -package grpc; +package signaling; service RpcSessions { rpc LookupResumeId(LookupResumeIdRequest) returns (LookupResumeIdReply) {} @@ -63,8 +63,7 @@ message IsSessionInCallReply { message GetInternalSessionsRequest { string roomId = 1; - string backendUrl = 2 [deprecated = true]; - repeated string backendUrls = 3; + string backendUrl = 2; } message InternalSessionData { diff --git a/grpc/sessions_grpc.pb.go b/grpc_sessions_grpc.pb.go similarity index 91% rename from grpc/sessions_grpc.pb.go rename to grpc_sessions_grpc.pb.go index c10dcb7..8b9c6a1 100644 --- a/grpc/sessions_grpc.pb.go +++ b/grpc_sessions_grpc.pb.go @@ -20,9 +20,9 @@ // along with this program. If not, see . // Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// source: grpc/sessions.proto +// source: grpc_sessions.proto -package grpc +package signaling import ( context "context" @@ -37,11 +37,11 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - RpcSessions_LookupResumeId_FullMethodName = "/grpc.RpcSessions/LookupResumeId" - RpcSessions_LookupSessionId_FullMethodName = "/grpc.RpcSessions/LookupSessionId" - RpcSessions_IsSessionInCall_FullMethodName = "/grpc.RpcSessions/IsSessionInCall" - RpcSessions_GetInternalSessions_FullMethodName = "/grpc.RpcSessions/GetInternalSessions" - RpcSessions_ProxySession_FullMethodName = "/grpc.RpcSessions/ProxySession" + RpcSessions_LookupResumeId_FullMethodName = "/signaling.RpcSessions/LookupResumeId" + RpcSessions_LookupSessionId_FullMethodName = "/signaling.RpcSessions/LookupSessionId" + RpcSessions_IsSessionInCall_FullMethodName = "/signaling.RpcSessions/IsSessionInCall" + RpcSessions_GetInternalSessions_FullMethodName = "/signaling.RpcSessions/GetInternalSessions" + RpcSessions_ProxySession_FullMethodName = "/signaling.RpcSessions/ProxySession" ) // RpcSessionsClient is the client API for RpcSessions service. @@ -136,19 +136,19 @@ type RpcSessionsServer interface { type UnimplementedRpcSessionsServer struct{} func (UnimplementedRpcSessionsServer) LookupResumeId(context.Context, *LookupResumeIdRequest) (*LookupResumeIdReply, error) { - return nil, status.Error(codes.Unimplemented, "method LookupResumeId not implemented") + return nil, status.Errorf(codes.Unimplemented, "method LookupResumeId not implemented") } func (UnimplementedRpcSessionsServer) LookupSessionId(context.Context, *LookupSessionIdRequest) (*LookupSessionIdReply, error) { - return nil, status.Error(codes.Unimplemented, "method LookupSessionId not implemented") + return nil, status.Errorf(codes.Unimplemented, "method LookupSessionId not implemented") } func (UnimplementedRpcSessionsServer) IsSessionInCall(context.Context, *IsSessionInCallRequest) (*IsSessionInCallReply, error) { - return nil, status.Error(codes.Unimplemented, "method IsSessionInCall not implemented") + return nil, status.Errorf(codes.Unimplemented, "method IsSessionInCall not implemented") } func (UnimplementedRpcSessionsServer) GetInternalSessions(context.Context, *GetInternalSessionsRequest) (*GetInternalSessionsReply, error) { - return nil, status.Error(codes.Unimplemented, "method GetInternalSessions not implemented") + return nil, status.Errorf(codes.Unimplemented, "method GetInternalSessions not implemented") } func (UnimplementedRpcSessionsServer) ProxySession(grpc.BidiStreamingServer[ClientSessionMessage, ServerSessionMessage]) error { - return status.Error(codes.Unimplemented, "method ProxySession not implemented") + return status.Errorf(codes.Unimplemented, "method ProxySession not implemented") } func (UnimplementedRpcSessionsServer) mustEmbedUnimplementedRpcSessionsServer() {} func (UnimplementedRpcSessionsServer) testEmbeddedByValue() {} @@ -161,7 +161,7 @@ type UnsafeRpcSessionsServer interface { } func RegisterRpcSessionsServer(s grpc.ServiceRegistrar, srv RpcSessionsServer) { - // If the following call panics, it indicates UnimplementedRpcSessionsServer was + // If the following call pancis, it indicates UnimplementedRpcSessionsServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -254,7 +254,7 @@ type RpcSessions_ProxySessionServer = grpc.BidiStreamingServer[ClientSessionMess // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var RpcSessions_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "grpc.RpcSessions", + ServiceName: "signaling.RpcSessions", HandlerType: (*RpcSessionsServer)(nil), Methods: []grpc.MethodDesc{ { @@ -282,5 +282,5 @@ var RpcSessions_ServiceDesc = grpc.ServiceDesc{ ClientStreams: true, }, }, - Metadata: "grpc/sessions.proto", + Metadata: "grpc_sessions.proto", } diff --git a/grpc/client_stats_prometheus.go b/grpc_stats_prometheus.go similarity index 74% rename from grpc/client_stats_prometheus.go rename to grpc_stats_prometheus.go index 8f7070e..fa37bdc 100644 --- a/grpc/client_stats_prometheus.go +++ b/grpc_stats_prometheus.go @@ -19,16 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package grpc +package signaling import ( "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" ) var ( - statsGrpcClients = prometheus.NewGauge(prometheus.GaugeOpts{ // +checklocksignore: Global readonly variable. + statsGrpcClients = prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: "signaling", Subsystem: "grpc", Name: "clients", @@ -45,8 +43,23 @@ var ( statsGrpcClients, statsGrpcClientCalls, } + + statsGrpcServerCalls = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "signaling", + Subsystem: "grpc", + Name: "server_calls_total", + Help: "The total number of GRPC server calls", + }, []string{"method"}) + + grpcServerStats = []prometheus.Collector{ + statsGrpcServerCalls, + } ) func RegisterGrpcClientStats() { - metrics.RegisterAll(grpcClientStats...) + registerAll(grpcClientStats...) +} + +func RegisterGrpcServerStats() { + registerAll(grpcServerStats...) } diff --git a/pool/http_client_pool.go b/http_client_pool.go similarity index 91% rename from pool/http_client_pool.go rename to http_client_pool.go index 55004be..65c19dc 100644 --- a/pool/http_client_pool.go +++ b/http_client_pool.go @@ -19,12 +19,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package pool +package signaling import ( "context" "crypto/tls" "errors" + "fmt" "net/http" "net/url" "sync" @@ -32,10 +33,6 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var ( - ErrNotRedirecting = errors.New("not redirecting to different host") -) - func init() { RegisterHttpClientPoolStats() } @@ -63,7 +60,7 @@ func (p *Pool) Put(c *http.Client) { func newPool(host string, constructor func() *http.Client, size int) (*Pool, error) { if size <= 0 { - return nil, errors.New("can't create empty pool") + return nil, fmt.Errorf("can't create empty pool") } p := &Pool{ @@ -82,15 +79,14 @@ type HttpClientPool struct { mu sync.Mutex transport *http.Transport - // +checklocks:mu - clients map[string]*Pool + clients map[string]*Pool - maxConcurrentRequestsPerHost int // +checklocksignore: Only written to from constructor. + maxConcurrentRequestsPerHost int } func NewHttpClientPool(maxConcurrentRequestsPerHost int, skipVerify bool) (*HttpClientPool, error) { if maxConcurrentRequestsPerHost <= 0 { - return nil, errors.New("can't create empty pool") + return nil, fmt.Errorf("can't create empty pool") } tlsconfig := &tls.Config{ diff --git a/pool/http_client_pool_stats_prometheus.go b/http_client_pool_stats_prometheus.go similarity index 91% rename from pool/http_client_pool_stats_prometheus.go rename to http_client_pool_stats_prometheus.go index 45a5fe6..72d7b2c 100644 --- a/pool/http_client_pool_stats_prometheus.go +++ b/http_client_pool_stats_prometheus.go @@ -19,12 +19,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package pool +package signaling import ( "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" ) var ( @@ -41,5 +39,5 @@ var ( ) func RegisterHttpClientPoolStats() { - metrics.RegisterAll(httpClientPoolStats...) + registerAll(httpClientPoolStats...) } diff --git a/pool/http_client_pool_test.go b/http_client_pool_test.go similarity index 99% rename from pool/http_client_pool_test.go rename to http_client_pool_test.go index 1433b5f..dff3866 100644 --- a/pool/http_client_pool_test.go +++ b/http_client_pool_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package pool +package signaling import ( "context" diff --git a/server/hub.go b/hub.go similarity index 51% rename from server/hub.go rename to hub.go index b97546e..747b435 100644 --- a/server/hub.go +++ b/hub.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "bytes" @@ -36,6 +36,7 @@ import ( "errors" "fmt" "hash/fnv" + "log" "net" "net/http" "net/url" @@ -44,58 +45,27 @@ import ( "sync" "sync/atomic" "time" - unsafe "unsafe" "github.com/dlintw/goconf" "github.com/golang-jwt/jwt/v5" "github.com/gorilla/mux" "github.com/gorilla/websocket" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/client" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/session" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" + "google.golang.org/protobuf/types/known/timestamppb" ) var ( - // HelloExpected is returned if a client sends a message before the "hello" request. - HelloExpected = api.NewError("hello_expected", "Expected Hello request.") - // UserAuthFailed is returned if the Talk response to a v1 hello is not an error but also not a valid auth response. - UserAuthFailed = api.NewError("auth_failed", "The user could not be authenticated.") - // RoomJoinFailed is returned if the Talk response to a room join request is not an error and not a valid join response. - RoomJoinFailed = api.NewError("room_join_failed", "Could not join the room.") - // InvalidClientType is returned if the client type in the "hello" request is not supported. - InvalidClientType = api.NewError("invalid_client_type", "The client type is not supported.") - // InvalidBackendUrl is returned if no backend is configured for URL in the "hello" request. - InvalidBackendUrl = api.NewError("invalid_backend", "The backend URL is not supported.") - // InvalidToken is returned if the token in a "hello" request could not be validated. - InvalidToken = api.NewError("invalid_token", "The passed token is invalid.") - // NoSuchSession is returned if the session to be resumed is unknown or expired. - NoSuchSession = api.NewError("no_such_session", "The session to resume does not exist.") - // TokenNotValidYet is returned if the token in a "hello" request could be authenticated but is not valid yet. - // This hints to a mismatch in the time between the server running Talk and the server running the signaling server. - TokenNotValidYet = api.NewError("token_not_valid_yet", "The token is not valid yet.") - // TokenExpired is returned if the token in a "hello" request could be authenticated but is expired. - // This hints to a mismatch in the time between the server running Talk and the server running the signaling server, - // but could also be a client trying to connect with an old token. - TokenExpired = api.NewError("token_expired", "The token is expired.") - // TooManyRequests is returned if brute force detection reports too many failed "hello" requests. - TooManyRequests = api.NewError("too_many_requests", "Too many requests.") - - ErrNoProxyTokenSupported = errors.New("proxy token generation not supported") + DuplicateClient = NewError("duplicate_client", "Client already registered.") + HelloExpected = NewError("hello_expected", "Expected Hello request.") + InvalidHelloVersion = NewError("invalid_hello_version", "The hello version is not supported.") + UserAuthFailed = NewError("auth_failed", "The user could not be authenticated.") + RoomJoinFailed = NewError("room_join_failed", "Could not join the room.") + InvalidClientType = NewError("invalid_client_type", "The client type is not supported.") + InvalidBackendUrl = NewError("invalid_backend", "The backend URL is not supported.") + InvalidToken = NewError("invalid_token", "The passed token is invalid.") + NoSuchSession = NewError("no_such_session", "The session to resume does not exist.") + TokenNotValidYet = NewError("token_not_valid_yet", "The token is not valid yet.") + TokenExpired = NewError("token_expired", "The token is expired.") + TooManyRequests = NewError("too_many_requests", "Too many requests.") // Maximum number of concurrent requests to a backend. defaultMaxConcurrentRequestsPerHost = 8 @@ -110,13 +80,13 @@ var ( defaultFederationTimeoutSeconds = 10 // New connections have to send a "Hello" request after 2 seconds. - initialHelloTimeout = 2 * time.Second // +checklocksignore: Global readonly variable. + initialHelloTimeout = 2 * time.Second // Anonymous clients have to join a room after 10 seconds. - anonmyousJoinRoomTimeout = 10 * time.Second // +checklocksignore: Global readonly variable. + anonmyousJoinRoomTimeout = 10 * time.Second // Sessions expire 30 seconds after the connection closed. - sessionExpireDuration = 30 * time.Second // +checklocksignore: Global readonly variable. + sessionExpireDuration = 30 * time.Second // Run housekeeping jobs once per second housekeepingInterval = time.Second @@ -135,8 +105,6 @@ var ( websocketReadBufferSize = 4096 websocketWriteBufferSize = 4096 - websocketWriteBufferPool = &sync.Pool{} - // Delay after which a screen publisher should be cleaned up. cleanupScreenPublisherDelay = time.Second @@ -145,113 +113,89 @@ var ( // Allow time differences of up to one minute between server and proxy. tokenLeeway = time.Minute + + DefaultTrustedProxies = DefaultPrivateIps() ) func init() { RegisterHubStats() } -type ClientWithSession interface { - client.HandlerClient - - IsAuthenticated() bool - GetSession() Session - SetSession(session Session) -} - type Hub struct { version string - logger log.Logger - events events.AsyncEvents + events AsyncEvents upgrader websocket.Upgrader - sessionIds *session.SessionIdCodec - info *api.WelcomeServerMessage - infoInternal *api.WelcomeServerMessage - welcome atomic.Value // *api.ServerMessage + cookie *SessionIdCodec + info *WelcomeServerMessage + infoInternal *WelcomeServerMessage + welcome atomic.Value // *ServerMessage - closer *internal.Closer + closer *Closer readPumpActive atomic.Int32 writePumpActive atomic.Int32 - shutdown *internal.Closer + shutdown *Closer shutdownScheduled atomic.Bool - roomUpdated chan *talk.BackendServerRoomRequest - roomDeleted chan *talk.BackendServerRoomRequest - roomInCall chan *talk.BackendServerRoomRequest - roomParticipants chan *talk.BackendServerRoomRequest + roomUpdated chan *BackendServerRoomRequest + roomDeleted chan *BackendServerRoomRequest + roomInCall chan *BackendServerRoomRequest + roomParticipants chan *BackendServerRoomRequest mu sync.RWMutex ru sync.RWMutex - sid atomic.Uint64 - // +checklocks:mu - clients map[uint64]ClientWithSession - // +checklocks:mu + sid atomic.Uint64 + clients map[uint64]HandlerClient sessions map[uint64]Session - // +checklocks:ru - rooms map[string]*Room + rooms map[string]*Room - roomSessions RoomSessions - roomPing *RoomPing - // +checklocks:mu - virtualSessions map[api.PublicSessionId]uint64 + roomSessions RoomSessions + roomPing *RoomPing + virtualSessions map[string]uint64 - decodeCaches []*container.LruCache[*session.SessionIdData] + decodeCaches []*LruCache - mcu sfu.SFU + mcu Mcu mcuTimeout time.Duration internalClientsSecret []byte allowSubscribeAnyStream bool - // +checklocks:mu - expiredSessions map[Session]time.Time - // +checklocks:mu - anonymousSessions map[*ClientSession]time.Time - // +checklocks:mu - expectHelloClients map[ClientWithSession]time.Time - // +checklocks:mu - dialoutSessions map[*ClientSession]bool - // +checklocks:mu - remoteSessions map[*RemoteSession]bool - // +checklocks:mu - federatedSessions map[*ClientSession]bool - // +checklocks:mu - federationClients map[*FederationClient]bool + expiredSessions map[Session]time.Time + anonymousSessions map[*ClientSession]time.Time + expectHelloClients map[HandlerClient]time.Time + dialoutSessions map[*ClientSession]bool + remoteSessions map[*RemoteSession]bool + federatedSessions map[*ClientSession]bool backendTimeout time.Duration - backend *talk.BackendClient + backend *BackendClient - trustedProxies atomic.Pointer[container.IPList] - geoip *geoip.Lookup - geoipOverrides geoip.AtomicOverrides + trustedProxies atomic.Pointer[AllowedIps] + geoip *GeoLookup + geoipOverrides atomic.Pointer[map[*net.IPNet]string] geoipUpdating atomic.Bool - etcdClient etcd.Client - rpcServer *grpc.Server - rpcClients *grpc.Clients + rpcServer *GrpcServer + rpcClients *GrpcClients - throttler async.Throttler + throttler Throttler skipFederationVerify bool federationTimeout time.Duration - - allowedCandidates atomic.Pointer[container.IPList] - blockedCandidates atomic.Pointer[container.IPList] } -func NewHub(ctx context.Context, cfg *goconf.ConfigFile, events events.AsyncEvents, rpcServer *grpc.Server, rpcClients *grpc.Clients, etcdClient etcd.Client, r *mux.Router, version string) (*Hub, error) { - logger := log.LoggerFromContext(ctx) - hashKey, _ := config.GetStringOptionWithEnv(cfg, "sessions", "hashkey") +func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer, rpcClients *GrpcClients, etcdClient *EtcdClient, r *mux.Router, version string) (*Hub, error) { + hashKey, _ := config.GetString("sessions", "hashkey") switch len(hashKey) { case 32: case 64: default: - logger.Printf("WARNING: The sessions hash key should be 32 or 64 bytes but is %d bytes", len(hashKey)) + log.Printf("WARNING: The sessions hash key should be 32 or 64 bytes but is %d bytes", len(hashKey)) } - blockKey, _ := config.GetStringOptionWithEnv(cfg, "sessions", "blockkey") + blockKey, _ := config.GetString("sessions", "blockkey") blockBytes := []byte(blockKey) switch len(blockKey) { case 0: @@ -263,71 +207,66 @@ func NewHub(ctx context.Context, cfg *goconf.ConfigFile, events events.AsyncEven return nil, fmt.Errorf("the sessions block key must be 16, 24 or 32 bytes but is %d bytes", len(blockKey)) } - sessionIds, err := session.NewSessionIdCodec([]byte(hashKey), blockBytes) - if err != nil { - return nil, fmt.Errorf("error creating session id codec: %w", err) - } - - internalClientsSecret, _ := config.GetStringOptionWithEnv(cfg, "clients", "internalsecret") + internalClientsSecret, _ := config.GetString("clients", "internalsecret") if internalClientsSecret == "" { - logger.Println("WARNING: No shared secret has been set for internal clients.") + log.Println("WARNING: No shared secret has been set for internal clients.") } - maxConcurrentRequestsPerHost, _ := cfg.GetInt("backend", "connectionsperhost") + maxConcurrentRequestsPerHost, _ := config.GetInt("backend", "connectionsperhost") if maxConcurrentRequestsPerHost <= 0 { maxConcurrentRequestsPerHost = defaultMaxConcurrentRequestsPerHost } - backend, err := talk.NewBackendClient(ctx, cfg, maxConcurrentRequestsPerHost, version, etcdClient) + backend, err := NewBackendClient(config, maxConcurrentRequestsPerHost, version, etcdClient) if err != nil { return nil, err } - logger.Printf("Using a maximum of %d concurrent backend connections per host", maxConcurrentRequestsPerHost) + log.Printf("Using a maximum of %d concurrent backend connections per host", maxConcurrentRequestsPerHost) - backendTimeoutSeconds, _ := cfg.GetInt("backend", "timeout") + backendTimeoutSeconds, _ := config.GetInt("backend", "timeout") if backendTimeoutSeconds <= 0 { backendTimeoutSeconds = defaultBackendTimeoutSeconds } backendTimeout := time.Duration(backendTimeoutSeconds) * time.Second - logger.Printf("Using a timeout of %s for backend connections", backendTimeout) + log.Printf("Using a timeout of %s for backend connections", backendTimeout) - mcuTimeoutSeconds, _ := cfg.GetInt("mcu", "timeout") + mcuTimeoutSeconds, _ := config.GetInt("mcu", "timeout") if mcuTimeoutSeconds <= 0 { mcuTimeoutSeconds = defaultMcuTimeoutSeconds } mcuTimeout := time.Duration(mcuTimeoutSeconds) * time.Second - allowSubscribeAnyStream, _ := cfg.GetBool("app", "allowsubscribeany") + allowSubscribeAnyStream, _ := config.GetBool("app", "allowsubscribeany") if allowSubscribeAnyStream { - logger.Printf("WARNING: Allow subscribing any streams, this is insecure and should only be enabled for testing") + log.Printf("WARNING: Allow subscribing any streams, this is insecure and should only be enabled for testing") } - trustedProxies, _ := cfg.GetString("app", "trustedproxies") - trustedProxiesIps, err := container.ParseIPList(trustedProxies) + trustedProxies, _ := config.GetString("app", "trustedproxies") + trustedProxiesIps, err := ParseAllowedIps(trustedProxies) if err != nil { return nil, err } - skipFederationVerify, _ := cfg.GetBool("federation", "skipverify") + skipFederationVerify, _ := config.GetBool("federation", "skipverify") if skipFederationVerify { - logger.Println("WARNING: Federation target verification is disabled!") + log.Println("WARNING: Federation target verification is disabled!") } - federationTimeoutSeconds, _ := cfg.GetInt("federation", "timeout") + federationTimeoutSeconds, _ := config.GetInt("federation", "timeout") if federationTimeoutSeconds <= 0 { federationTimeoutSeconds = defaultFederationTimeoutSeconds } federationTimeout := time.Duration(federationTimeoutSeconds) * time.Second if !trustedProxiesIps.Empty() { - logger.Printf("Trusted proxies: %s", trustedProxiesIps) + log.Printf("Trusted proxies: %s", trustedProxiesIps) } else { - trustedProxiesIps = client.DefaultTrustedProxies - logger.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps) + trustedProxiesIps = DefaultTrustedProxies + log.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps) } - decodeCaches := make([]*container.LruCache[*session.SessionIdData], 0, numDecodeCaches) - for range numDecodeCaches { - decodeCaches = append(decodeCaches, container.NewLruCache[*session.SessionIdData](decodeCacheSize)) + decodeCaches := make([]*LruCache, 0, numDecodeCaches) + for i := 0; i < numDecodeCaches; i++ { + decodeCaches = append(decodeCaches, NewLruCache(decodeCacheSize)) } roomSessions, err := NewBuiltinRoomSessions(rpcClients) @@ -335,78 +274,74 @@ func NewHub(ctx context.Context, cfg *goconf.ConfigFile, events events.AsyncEven return nil, err } - roomPing, err := NewRoomPing(backend) + roomPing, err := NewRoomPing(backend, backend.capabilities) if err != nil { return nil, err } - geoipUrl, _ := cfg.GetString("geoip", "url") + geoipUrl, _ := config.GetString("geoip", "url") if geoipUrl == "default" || geoipUrl == "none" { geoipUrl = "" } if geoipUrl == "" { - if geoipLicense, _ := cfg.GetString("geoip", "license"); geoipLicense != "" { - geoipUrl = geoip.GetMaxMindDownloadUrl(geoipLicense) + if geoipLicense, _ := config.GetString("geoip", "license"); geoipLicense != "" { + geoipUrl = GetGeoIpDownloadUrl(geoipLicense) } } - var geoipLookup *geoip.Lookup + var geoip *GeoLookup if geoipUrl != "" { - if geoipUrl, found := strings.CutPrefix(geoipUrl, "file://"); found { - logger.Printf("Using GeoIP database from %s", geoipUrl) - geoipLookup, err = geoip.NewLookupFromFile(logger, geoipUrl) + if strings.HasPrefix(geoipUrl, "file://") { + geoipUrl = geoipUrl[7:] + log.Printf("Using GeoIP database from %s", geoipUrl) + geoip, err = NewGeoLookupFromFile(geoipUrl) } else { - logger.Printf("Downloading GeoIP database from %s", geoipUrl) - geoipLookup, err = geoip.NewLookupFromUrl(logger, geoipUrl) + log.Printf("Downloading GeoIP database from %s", geoipUrl) + geoip, err = NewGeoLookupFromUrl(geoipUrl) } if err != nil { return nil, err } } else { - logger.Printf("Not using GeoIP database") + log.Printf("Not using GeoIP database") } - geoipOverrides, err := geoip.LoadOverrides(ctx, cfg, false) + geoipOverrides, err := LoadGeoIPOverrides(config, false) if err != nil { return nil, err } - throttler, err := async.NewMemoryThrottler() + throttler, err := NewMemoryThrottler() if err != nil { return nil, err } hub := &Hub{ version: version, - logger: logger, events: events, upgrader: websocket.Upgrader{ ReadBufferSize: websocketReadBufferSize, WriteBufferSize: websocketWriteBufferSize, - WriteBufferPool: websocketWriteBufferPool, - Subprotocols: []string{ - janus.EventsSubprotocol, - }, }, - sessionIds: sessionIds, - info: api.NewWelcomeServerMessage(version, api.DefaultFeatures...), - infoInternal: api.NewWelcomeServerMessage(version, api.DefaultFeaturesInternal...), + cookie: NewSessionIdCodec([]byte(hashKey), blockBytes), + info: NewWelcomeServerMessage(version, DefaultFeatures...), + infoInternal: NewWelcomeServerMessage(version, DefaultFeaturesInternal...), - closer: internal.NewCloser(), - shutdown: internal.NewCloser(), + closer: NewCloser(), + shutdown: NewCloser(), - roomUpdated: make(chan *talk.BackendServerRoomRequest), - roomDeleted: make(chan *talk.BackendServerRoomRequest), - roomInCall: make(chan *talk.BackendServerRoomRequest), - roomParticipants: make(chan *talk.BackendServerRoomRequest), + roomUpdated: make(chan *BackendServerRoomRequest), + roomDeleted: make(chan *BackendServerRoomRequest), + roomInCall: make(chan *BackendServerRoomRequest), + roomParticipants: make(chan *BackendServerRoomRequest), - clients: make(map[uint64]ClientWithSession), + clients: make(map[uint64]HandlerClient), sessions: make(map[uint64]Session), rooms: make(map[string]*Room), roomSessions: roomSessions, roomPing: roomPing, - virtualSessions: make(map[api.PublicSessionId]uint64), + virtualSessions: make(map[string]uint64), decodeCaches: decodeCaches, @@ -417,18 +352,16 @@ func NewHub(ctx context.Context, cfg *goconf.ConfigFile, events events.AsyncEven expiredSessions: make(map[Session]time.Time), anonymousSessions: make(map[*ClientSession]time.Time), - expectHelloClients: make(map[ClientWithSession]time.Time), + expectHelloClients: make(map[HandlerClient]time.Time), dialoutSessions: make(map[*ClientSession]bool), remoteSessions: make(map[*RemoteSession]bool), federatedSessions: make(map[*ClientSession]bool), - federationClients: make(map[*FederationClient]bool), backendTimeout: backendTimeout, backend: backend, - geoip: geoipLookup, + geoip: geoip, - etcdClient: etcdClient, rpcServer: rpcServer, rpcClients: rpcClients, @@ -437,42 +370,17 @@ func NewHub(ctx context.Context, cfg *goconf.ConfigFile, events events.AsyncEven skipFederationVerify: skipFederationVerify, federationTimeout: federationTimeout, } - if value, _ := cfg.GetString("mcu", "allowedcandidates"); value != "" { - allowed, err := container.ParseIPList(value) - if err != nil { - return nil, fmt.Errorf("invalid allowedcandidates: %w", err) - } - - logger.Printf("Candidates allowlist: %s", allowed) - hub.allowedCandidates.Store(allowed) - } else { - logger.Printf("No candidates allowlist") - } - if value, _ := cfg.GetString("mcu", "blockedcandidates"); value != "" { - blocked, err := container.ParseIPList(value) - if err != nil { - return nil, fmt.Errorf("invalid blockedcandidates: %w", err) - } - - logger.Printf("Candidates blocklist: %s", blocked) - hub.blockedCandidates.Store(blocked) - } else { - logger.Printf("No candidates blocklist") - } - hub.trustedProxies.Store(trustedProxiesIps) - hub.geoipOverrides.Store(geoipOverrides) - - hub.setWelcomeMessage(&api.ServerMessage{ + if len(geoipOverrides) > 0 { + hub.geoipOverrides.Store(&geoipOverrides) + } + hub.setWelcomeMessage(&ServerMessage{ Type: "welcome", - Welcome: api.NewWelcomeServerMessage(version, api.DefaultWelcomeFeatures...), + Welcome: NewWelcomeServerMessage(version, DefaultWelcomeFeatures...), }) - backend.SetFeaturesFunc(func() []string { - return hub.info.Features - }) - roomPing.hub = hub + backend.hub = hub if rpcServer != nil { - rpcServer.SetHub(hub) + rpcServer.hub = hub } hub.upgrader.CheckOrigin = hub.checkOrigin r.HandleFunc("/spreed", func(w http.ResponseWriter, r *http.Request) { @@ -482,29 +390,29 @@ func NewHub(ctx context.Context, cfg *goconf.ConfigFile, events events.AsyncEven return hub, nil } -func (h *Hub) setWelcomeMessage(msg *api.ServerMessage) { +func (h *Hub) setWelcomeMessage(msg *ServerMessage) { h.welcome.Store(msg) } -func (h *Hub) getWelcomeMessage() *api.ServerMessage { - return h.welcome.Load().(*api.ServerMessage) +func (h *Hub) getWelcomeMessage() *ServerMessage { + return h.welcome.Load().(*ServerMessage) } -func (h *Hub) SetMcu(mcu sfu.SFU) { +func (h *Hub) SetMcu(mcu Mcu) { h.mcu = mcu // Create copy of message so it can be updated concurrently. welcome := *h.getWelcomeMessage() if mcu == nil { - h.info.RemoveFeature(api.ServerFeatureMcu, api.ServerFeatureSimulcast, api.ServerFeatureUpdateSdp) - h.infoInternal.RemoveFeature(api.ServerFeatureMcu, api.ServerFeatureSimulcast, api.ServerFeatureUpdateSdp) + h.info.RemoveFeature(ServerFeatureMcu, ServerFeatureSimulcast, ServerFeatureUpdateSdp) + h.infoInternal.RemoveFeature(ServerFeatureMcu, ServerFeatureSimulcast, ServerFeatureUpdateSdp) - welcome.Welcome.RemoveFeature(api.ServerFeatureMcu, api.ServerFeatureSimulcast, api.ServerFeatureUpdateSdp) + welcome.Welcome.RemoveFeature(ServerFeatureMcu, ServerFeatureSimulcast, ServerFeatureUpdateSdp) } else { - h.logger.Printf("Using a timeout of %s for MCU requests", h.mcuTimeout) - h.info.AddFeature(api.ServerFeatureMcu, api.ServerFeatureSimulcast, api.ServerFeatureUpdateSdp) - h.infoInternal.AddFeature(api.ServerFeatureMcu, api.ServerFeatureSimulcast, api.ServerFeatureUpdateSdp) + log.Printf("Using a timeout of %s for MCU requests", h.mcuTimeout) + h.info.AddFeature(ServerFeatureMcu, ServerFeatureSimulcast, ServerFeatureUpdateSdp) + h.infoInternal.AddFeature(ServerFeatureMcu, ServerFeatureSimulcast, ServerFeatureUpdateSdp) - welcome.Welcome.AddFeature(api.ServerFeatureMcu, api.ServerFeatureSimulcast, api.ServerFeatureUpdateSdp) + welcome.Welcome.AddFeature(ServerFeatureMcu, ServerFeatureSimulcast, ServerFeatureUpdateSdp) } h.setWelcomeMessage(&welcome) } @@ -514,8 +422,8 @@ func (h *Hub) checkOrigin(r *http.Request) bool { return true } -func (h *Hub) GetServerInfo(session Session) *api.WelcomeServerMessage { - if session.ClientType() == api.HelloClientTypeInternal { +func (h *Hub) GetServerInfo(session Session) *WelcomeServerMessage { + if session.ClientType() == HelloClientTypeInternal { return h.infoInternal } @@ -533,9 +441,9 @@ func (h *Hub) updateGeoDatabase() { } defer h.geoipUpdating.Store(false) - backoff, err := async.NewExponentialBackoff(time.Second, 5*time.Minute) + backoff, err := NewExponentialBackoff(time.Second, 5*time.Minute) if err != nil { - h.logger.Printf("Could not create exponential backoff: %s", err) + log.Printf("Could not create exponential backoff: %s", err) return } @@ -545,7 +453,7 @@ func (h *Hub) updateGeoDatabase() { break } - h.logger.Printf("Could not update GeoIP database, will retry in %s (%s)", backoff.NextWait(), err) + log.Printf("Could not update GeoIP database, will retry in %s (%s)", backoff.NextWait(), err) backoff.Wait(context.Background()) } } @@ -593,44 +501,25 @@ func (h *Hub) Stop() { h.throttler.Close() } -func (h *Hub) Reload(ctx context.Context, config *goconf.ConfigFile) { +func (h *Hub) Reload(config *goconf.ConfigFile) { trustedProxies, _ := config.GetString("app", "trustedproxies") - if trustedProxiesIps, err := container.ParseIPList(trustedProxies); err == nil { + if trustedProxiesIps, err := ParseAllowedIps(trustedProxies); err == nil { if !trustedProxiesIps.Empty() { - h.logger.Printf("Trusted proxies: %s", trustedProxiesIps) + log.Printf("Trusted proxies: %s", trustedProxiesIps) } else { - trustedProxiesIps = client.DefaultTrustedProxies - h.logger.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps) + trustedProxiesIps = DefaultTrustedProxies + log.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps) } h.trustedProxies.Store(trustedProxiesIps) } else { - h.logger.Printf("Error parsing trusted proxies from \"%s\": %s", trustedProxies, err) + log.Printf("Error parsing trusted proxies from \"%s\": %s", trustedProxies, err) } - geoipOverrides, _ := geoip.LoadOverrides(ctx, config, true) - h.geoipOverrides.Store(geoipOverrides) - - if value, _ := config.GetString("mcu", "allowedcandidates"); value != "" { - if allowed, err := container.ParseIPList(value); err != nil { - h.logger.Printf("invalid allowedcandidates: %s", err) - } else { - h.logger.Printf("Candidates allowlist: %s", allowed) - h.allowedCandidates.Store(allowed) - } + geoipOverrides, _ := LoadGeoIPOverrides(config, true) + if len(geoipOverrides) > 0 { + h.geoipOverrides.Store(&geoipOverrides) } else { - h.logger.Printf("No candidates allowlist") - h.allowedCandidates.Store(nil) - } - if value, _ := config.GetString("mcu", "blockedcandidates"); value != "" { - if blocked, err := container.ParseIPList(value); err != nil { - h.logger.Printf("invalid blockedcandidates: %s", err) - } else { - h.logger.Printf("Candidates blocklist: %s", blocked) - h.blockedCandidates.Store(blocked) - } - } else { - h.logger.Printf("No candidates blocklist") - h.blockedCandidates.Store(nil) + h.geoipOverrides.Store(nil) } if h.mcu != nil { @@ -640,60 +529,45 @@ func (h *Hub) Reload(ctx context.Context, config *goconf.ConfigFile) { h.rpcClients.Reload(config) } -func (h *Hub) getDecodeCache(cache_key string) *container.LruCache[*session.SessionIdData] { +func (h *Hub) getDecodeCache(cache_key string) *LruCache { hash := fnv.New32a() - // Make sure we don't have a temporary allocation for the string -> []byte conversion. - hash.Write(unsafe.Slice(unsafe.StringData(cache_key), len(cache_key))) // nolint + hash.Write([]byte(cache_key)) // nolint idx := hash.Sum32() % uint32(len(h.decodeCaches)) return h.decodeCaches[idx] } -func (h *Hub) invalidatePublicSessionId(id api.PublicSessionId) { - h.invalidateSessionId(string(id)) -} - -func (h *Hub) invalidatePrivateSessionId(id api.PrivateSessionId) { - h.invalidateSessionId(string(id)) -} - -func (h *Hub) invalidateSessionId(id string) { +func (h *Hub) invalidateSessionId(id string, sessionType string) { if len(id) == 0 { return } - cache := h.getDecodeCache(id) - cache.Remove(id) + cache_key := id + "|" + sessionType + cache := h.getDecodeCache(cache_key) + cache.Remove(cache_key) } -func (h *Hub) setDecodedPublicSessionId(id api.PublicSessionId, data *session.SessionIdData) { - h.setDecodedSessionId(string(id), data) -} - -func (h *Hub) setDecodedPrivateSessionId(id api.PrivateSessionId, data *session.SessionIdData) { - h.setDecodedSessionId(string(id), data) -} - -func (h *Hub) setDecodedSessionId(id string, data *session.SessionIdData) { +func (h *Hub) setDecodedSessionId(id string, sessionType string, data *SessionIdData) { if len(id) == 0 { return } - cache := h.getDecodeCache(id) - cache.Set(id, data) + cache_key := id + "|" + sessionType + cache := h.getDecodeCache(cache_key) + cache.Set(cache_key, data) } -func (h *Hub) decodePrivateSessionId(id api.PrivateSessionId) *session.SessionIdData { +func (h *Hub) decodePrivateSessionId(id string) *SessionIdData { if len(id) == 0 { return nil } - cache_key := string(id) + cache_key := id + "|" + privateSessionName cache := h.getDecodeCache(cache_key) if result := cache.Get(cache_key); result != nil { - return result + return result.(*SessionIdData) } - data, err := h.sessionIds.DecodePrivate(id) + data, err := h.cookie.DecodePrivate(id) if err != nil { return nil } @@ -702,18 +576,18 @@ func (h *Hub) decodePrivateSessionId(id api.PrivateSessionId) *session.SessionId return data } -func (h *Hub) decodePublicSessionId(id api.PublicSessionId) *session.SessionIdData { +func (h *Hub) decodePublicSessionId(id string) *SessionIdData { if len(id) == 0 { return nil } - cache_key := string(id) + cache_key := id + "|" + publicSessionName cache := h.getDecodeCache(cache_key) if result := cache.Get(cache_key); result != nil { - return result + return result.(*SessionIdData) } - data, err := h.sessionIds.DecodePublic(id) + data, err := h.cookie.DecodePublic(id) if err != nil { return nil } @@ -722,7 +596,7 @@ func (h *Hub) decodePublicSessionId(id api.PublicSessionId) *session.SessionIdDa return data } -func (h *Hub) GetSessionByPublicId(sessionId api.PublicSessionId) Session { +func (h *Hub) GetSessionByPublicId(sessionId string) Session { data := h.decodePublicSessionId(sessionId) if data == nil { return nil @@ -738,7 +612,7 @@ func (h *Hub) GetSessionByPublicId(sessionId api.PublicSessionId) Session { return session } -func (h *Hub) GetSessionByResumeId(resumeId api.PrivateSessionId) Session { +func (h *Hub) GetSessionByResumeId(resumeId string) Session { data := h.decodePrivateSessionId(resumeId) if data == nil { return nil @@ -754,110 +628,40 @@ func (h *Hub) GetSessionByResumeId(resumeId api.PrivateSessionId) Session { return session } -func (h *Hub) GetSessionIdByResumeId(resumeId api.PrivateSessionId) api.PublicSessionId { - session := h.GetSessionByResumeId(resumeId) - if session == nil { - return "" - } - - return session.PublicId() -} - -func (h *Hub) GetSessionIdByRoomSessionId(roomSessionId api.RoomSessionId) (api.PublicSessionId, error) { +func (h *Hub) GetSessionIdByRoomSessionId(roomSessionId string) (string, error) { return h.roomSessions.GetSessionId(roomSessionId) } -func (h *Hub) IsSessionIdInCall(sessionId api.PublicSessionId, roomId string, backendUrl string) (bool, bool) { - session := h.GetSessionByPublicId(sessionId) - if session == nil { - return false, false - } +func (h *Hub) GetDialoutSession(roomId string, backend *Backend) *ClientSession { + url := backend.Url() - inCall := true - room := session.GetRoom() - if room == nil || room.Id() != roomId || !room.Backend().HasUrl(backendUrl) || - (session.ClientType() != api.HelloClientTypeInternal && !room.IsSessionInCall(session)) { - // Recipient is not in a room, a different room or not in the call. - inCall = false - } - - return inCall, true -} - -func (h *Hub) GetPublisherIdForSessionId(ctx context.Context, sessionId api.PublicSessionId, streamType sfu.StreamType) (*grpc.GetPublisherIdReply, error) { - session := h.GetSessionByPublicId(sessionId) - if session == nil { - return nil, status.Error(codes.NotFound, "no such session") - } - - clientSession, ok := session.(*ClientSession) - if !ok { - return nil, status.Error(codes.NotFound, "no such session") - } - - publisher := clientSession.GetOrWaitForPublisher(ctx, streamType) - if publisher, ok := publisher.(sfu.PublisherWithConnectionUrlAndIP); ok { - connUrl, ip := publisher.GetConnectionURL() - reply := &grpc.GetPublisherIdReply{ - PublisherId: publisher.Id(), - ProxyUrl: connUrl, - } - if len(ip) > 0 { - reply.Ip = ip.String() - } - var err error - if reply.ConnectToken, err = h.CreateProxyToken(""); err != nil && !errors.Is(err, ErrNoProxyTokenSupported) { - h.logger.Printf("Error creating proxy token for connection: %s", err) - return nil, status.Error(codes.Internal, "error creating proxy connect token") - } - if reply.PublisherToken, err = h.CreateProxyToken(publisher.Id()); err != nil && !errors.Is(err, ErrNoProxyTokenSupported) { - h.logger.Printf("Error creating proxy token for publisher %s: %s", publisher.Id(), err) - return nil, status.Error(codes.Internal, "error creating proxy publisher token") - } - return reply, nil - } - - return nil, status.Error(codes.NotFound, "no such publisher") -} - -func (h *Hub) GetDialoutSessions(roomId string, backend *talk.Backend) (result []*ClientSession) { h.mu.RLock() defer h.mu.RUnlock() for session := range h.dialoutSessions { - if !backend.HasUrl(session.BackendUrl()) { + if session.backend.Url() != url { continue } if session.GetClient() != nil { - result = append(result, session) + return session } } - return + return nil } -func (h *Hub) GetBackend(u *url.URL) *talk.Backend { +func (h *Hub) GetBackend(u *url.URL) *Backend { if u == nil { return h.backend.GetCompatBackend() } return h.backend.GetBackend(u) } -func (h *Hub) CreateProxyToken(publisherId string) (string, error) { - withToken, ok := h.mcu.(sfu.WithToken) - if !ok { - return "", ErrNoProxyTokenSupported - } - - return withToken.CreateToken(publisherId) -} - -// +checklocks:h.mu func (h *Hub) checkExpiredSessions(now time.Time) { for session, expires := range h.expiredSessions { if now.After(expires) { h.mu.Unlock() - h.logger.Printf("Closing expired session %s (private=%s)", session.PublicId(), session.PrivateId()) + log.Printf("Closing expired session %s (private=%s)", session.PublicId(), session.PrivateId()) session.Close() h.mu.Lock() // Should already be deleted by the close code, but better be sure. @@ -866,7 +670,6 @@ func (h *Hub) checkExpiredSessions(now time.Time) { } } -// +checklocks:h.mu func (h *Hub) checkAnonymousSessions(now time.Time) { for session, timeout := range h.anonymousSessions { if now.After(timeout) { @@ -881,7 +684,6 @@ func (h *Hub) checkAnonymousSessions(now time.Time) { } } -// +checklocks:h.mu func (h *Hub) checkInitialHello(now time.Time) { for client, timeout := range h.expectHelloClients { if now.After(timeout) { @@ -895,30 +697,28 @@ func (h *Hub) checkInitialHello(now time.Time) { func (h *Hub) performHousekeeping(now time.Time) { h.mu.Lock() - defer h.mu.Unlock() - h.checkExpiredSessions(now) h.checkAnonymousSessions(now) h.checkInitialHello(now) + h.mu.Unlock() } func (h *Hub) removeSession(session Session) (removed bool) { session.LeaveRoom(true) - h.invalidatePrivateSessionId(session.PrivateId()) - h.invalidatePublicSessionId(session.PublicId()) + h.invalidateSessionId(session.PrivateId(), privateSessionName) + h.invalidateSessionId(session.PublicId(), publicSessionName) h.mu.Lock() if data := session.Data(); data != nil && data.Sid > 0 { delete(h.clients, data.Sid) if _, found := h.sessions[data.Sid]; found { delete(h.sessions, data.Sid) - statsHubSessionsCurrent.WithLabelValues(session.Backend().Id(), string(session.ClientType())).Dec() + statsHubSessionsCurrent.WithLabelValues(session.Backend().Id(), session.ClientType()).Dec() removed = true } } delete(h.expiredSessions, session) if session, ok := session.(*ClientSession); ok { - delete(h.federatedSessions, session) delete(h.anonymousSessions, session) delete(h.dialoutSessions, session) } @@ -929,21 +729,13 @@ func (h *Hub) removeSession(session Session) (removed bool) { return } -func (h *Hub) removeFederationClient(client *FederationClient) { - h.mu.Lock() - defer h.mu.Unlock() - - delete(h.federationClients, client) -} - -// +checklocksread:h.mu func (h *Hub) hasSessionsLocked(withInternal bool) bool { if withInternal { return len(h.sessions) > 0 } for _, s := range h.sessions { - if s.ClientType() != api.HelloClientTypeInternal { + if s.ClientType() != HelloClientTypeInternal { return true } } @@ -958,9 +750,8 @@ func (h *Hub) startWaitAnonymousSessionRoom(session *ClientSession) { h.startWaitAnonymousSessionRoomLocked(session) } -// +checklocks:h.mu func (h *Hub) startWaitAnonymousSessionRoomLocked(session *ClientSession) { - if session.ClientType() == api.HelloClientTypeInternal { + if session.ClientType() == HelloClientTypeInternal { // Internal clients don't need to join a room. return } @@ -971,7 +762,7 @@ func (h *Hub) startWaitAnonymousSessionRoomLocked(session *ClientSession) { h.anonymousSessions[session] = now.Add(anonmyousJoinRoomTimeout) } -func (h *Hub) startExpectHello(client ClientWithSession) { +func (h *Hub) startExpectHello(client HandlerClient) { h.mu.Lock() defer h.mu.Unlock() if !client.IsConnected() { @@ -987,16 +778,16 @@ func (h *Hub) startExpectHello(client ClientWithSession) { h.expectHelloClients[client] = now.Add(initialHelloTimeout) } -func (h *Hub) processNewClient(client ClientWithSession) { +func (h *Hub) processNewClient(client HandlerClient) { h.startExpectHello(client) h.sendWelcome(client) } -func (h *Hub) sendWelcome(client ClientWithSession) { +func (h *Hub) sendWelcome(client HandlerClient) { client.SendMessage(h.getWelcomeMessage()) } -func (h *Hub) registerClient(client ClientWithSession) uint64 { +func (h *Hub) registerClient(client HandlerClient) uint64 { sid := h.sid.Add(1) for sid == 0 { sid = h.sid.Add(1) @@ -1020,40 +811,47 @@ func (h *Hub) unregisterRemoteSession(session *RemoteSession) { delete(h.remoteSessions, session) } -func (h *Hub) newSessionIdData(backend *talk.Backend) *session.SessionIdData { +func (h *Hub) newSessionIdData(backend *Backend) *SessionIdData { sid := h.sid.Add(1) for sid == 0 { sid = h.sid.Add(1) } - sessionIdData := &session.SessionIdData{ + sessionIdData := &SessionIdData{ Sid: sid, - Created: time.Now().UnixMicro(), + Created: timestamppb.Now(), BackendId: backend.Id(), } return sessionIdData } -func (h *Hub) processRegister(client ClientWithSession, message *api.ClientMessage, backend *talk.Backend, auth *talk.BackendClientResponse) { - if !client.IsConnected() { +func (h *Hub) processRegister(c HandlerClient, message *ClientMessage, backend *Backend, auth *BackendClientResponse) { + if !c.IsConnected() { // Client disconnected while waiting for "hello" response. return } if auth.Type == "error" { - client.SendMessage(message.NewErrorServerMessage(auth.Error)) + c.SendMessage(message.NewErrorServerMessage(auth.Error)) return } else if auth.Type != "auth" { - client.SendMessage(message.NewErrorServerMessage(UserAuthFailed)) + c.SendMessage(message.NewErrorServerMessage(UserAuthFailed)) + return + } + + client, ok := c.(*Client) + if !ok { + log.Printf("Can't register non-client %T", c) + client.SendMessage(message.NewWrappedErrorServerMessage(errors.New("can't register non-client"))) return } sessionIdData := h.newSessionIdData(backend) - privateSessionId, err := h.sessionIds.EncodePrivate(sessionIdData) + privateSessionId, err := h.cookie.EncodePrivate(sessionIdData) if err != nil { client.SendMessage(message.NewWrappedErrorServerMessage(err)) return } - publicSessionId, err := h.sessionIds.EncodePublic(sessionIdData) + publicSessionId, err := h.cookie.EncodePublic(sessionIdData) if err != nil { client.SendMessage(message.NewWrappedErrorServerMessage(err)) return @@ -1061,11 +859,11 @@ func (h *Hub) processRegister(client ClientWithSession, message *api.ClientMessa userId := auth.Auth.UserId if userId != "" { - h.logger.Printf("Register user %s@%s from %s in %s (%s) %s (private=%s)", userId, backend.Id(), client.RemoteAddr(), client.Country(), client.UserAgent(), publicSessionId, privateSessionId) - } else if message.Hello.Auth.Type != api.HelloClientTypeClient { - h.logger.Printf("Register %s@%s from %s in %s (%s) %s (private=%s)", message.Hello.Auth.Type, backend.Id(), client.RemoteAddr(), client.Country(), client.UserAgent(), publicSessionId, privateSessionId) + log.Printf("Register user %s@%s from %s in %s (%s) %s (private=%s)", userId, backend.Id(), client.RemoteAddr(), client.Country(), client.UserAgent(), publicSessionId, privateSessionId) + } else if message.Hello.Auth.Type != HelloClientTypeClient { + log.Printf("Register %s@%s from %s in %s (%s) %s (private=%s)", message.Hello.Auth.Type, backend.Id(), client.RemoteAddr(), client.Country(), client.UserAgent(), publicSessionId, privateSessionId) } else { - h.logger.Printf("Register anonymous@%s from %s in %s (%s) %s (private=%s)", backend.Id(), client.RemoteAddr(), client.Country(), client.UserAgent(), publicSessionId, privateSessionId) + log.Printf("Register anonymous@%s from %s in %s (%s) %s (private=%s)", backend.Id(), client.RemoteAddr(), client.Country(), client.UserAgent(), publicSessionId, privateSessionId) } session, err := NewClientSession(h, privateSessionId, publicSessionId, sessionIdData, backend, message.Hello, auth.Auth) @@ -1075,7 +873,7 @@ func (h *Hub) processRegister(client ClientWithSession, message *api.ClientMessa } if err := backend.AddSession(session); err != nil { - h.logger.Printf("Error adding session %s to backend %s: %s", session.PublicId(), backend.Id(), err) + log.Printf("Error adding session %s to backend %s: %s", session.PublicId(), backend.Id(), err) session.Close() client.SendMessage(message.NewWrappedErrorServerMessage(err)) return @@ -1087,26 +885,29 @@ func (h *Hub) processRegister(client ClientWithSession, message *api.ClientMessa var wg sync.WaitGroup ctx, cancel := context.WithTimeout(client.Context(), time.Second) defer cancel() - for _, grpcClient := range h.rpcClients.GetClients() { - wg.Go(func() { - count, err := grpcClient.GetSessionCount(ctx, session.BackendUrl()) + for _, client := range h.rpcClients.GetClients() { + wg.Add(1) + go func(c *GrpcClient) { + defer wg.Done() + + count, err := c.GetSessionCount(ctx, backend.ParsedUrl()) if err != nil { - h.logger.Printf("Received error while getting session count for %s from %s: %s", session.BackendUrl(), grpcClient.Target(), err) + log.Printf("Received error while getting session count for %s from %s: %s", backend.Url(), c.Target(), err) return } if count > 0 { - h.logger.Printf("%d sessions connected for %s on %s", count, session.BackendUrl(), grpcClient.Target()) + log.Printf("%d sessions connected for %s on %s", count, backend.Url(), c.Target()) totalCount.Add(count) } - }) + }(client) } wg.Wait() if totalCount.Load() > limit { backend.RemoveSession(session) - h.logger.Printf("Error adding session %s to backend %s: %s", session.PublicId(), backend.Id(), talk.SessionLimitExceeded) + log.Printf("Error adding session %s to backend %s: %s", session.PublicId(), backend.Id(), SessionLimitExceeded) session.Close() - client.SendMessage(message.NewWrappedErrorServerMessage(talk.SessionLimitExceeded)) + client.SendMessage(message.NewWrappedErrorServerMessage(SessionLimitExceeded)) return } } @@ -1124,27 +925,27 @@ func (h *Hub) processRegister(client ClientWithSession, message *api.ClientMessa h.sessions[sessionIdData.Sid] = session h.clients[sessionIdData.Sid] = client delete(h.expectHelloClients, client) - if userId == "" && session.ClientType() != api.HelloClientTypeInternal { + if userId == "" && session.ClientType() != HelloClientTypeInternal { h.startWaitAnonymousSessionRoomLocked(session) - } else if session.ClientType() == api.HelloClientTypeInternal && session.HasFeature(api.ClientFeatureStartDialout) { + } else if session.ClientType() == HelloClientTypeInternal && session.HasFeature(ClientFeatureStartDialout) { // TODO: There is a small race condition for sessions that take some time // between connecting and joining a room. h.dialoutSessions[session] = true } h.mu.Unlock() - if country := client.Country(); geoip.IsValidCountry(country) { - statsClientCountries.WithLabelValues(string(country)).Inc() + if country := client.Country(); IsValidCountry(country) { + statsClientCountries.WithLabelValues(country).Inc() } - statsHubSessionsCurrent.WithLabelValues(backend.Id(), string(session.ClientType())).Inc() - statsHubSessionsTotal.WithLabelValues(backend.Id(), string(session.ClientType())).Inc() + statsHubSessionsCurrent.WithLabelValues(backend.Id(), session.ClientType()).Inc() + statsHubSessionsTotal.WithLabelValues(backend.Id(), session.ClientType()).Inc() - h.setDecodedPrivateSessionId(privateSessionId, sessionIdData) - h.setDecodedPublicSessionId(publicSessionId, sessionIdData) + h.setDecodedSessionId(privateSessionId, privateSessionName, sessionIdData) + h.setDecodedSessionId(publicSessionId, publicSessionName, sessionIdData) h.sendHelloResponse(session, message) } -func (h *Hub) processUnregister(client ClientWithSession) Session { +func (h *Hub) processUnregister(client HandlerClient) Session { session := client.GetSession() h.mu.Lock() @@ -1156,9 +957,11 @@ func (h *Hub) processUnregister(client ClientWithSession) Session { } h.mu.Unlock() if session != nil { - h.logger.Printf("Unregister %s (private=%s)", session.PublicId(), session.PrivateId()) - if cs, ok := session.(*ClientSession); ok { - cs.ClearClient(client) + log.Printf("Unregister %s (private=%s)", session.PublicId(), session.PrivateId()) + if c, ok := client.(*Client); ok { + if cs, ok := session.(*ClientSession); ok { + cs.ClearClient(c) + } } } @@ -1166,14 +969,14 @@ func (h *Hub) processUnregister(client ClientWithSession) Session { return session } -func (h *Hub) processMessage(client ClientWithSession, data []byte) { - var message api.ClientMessage +func (h *Hub) processMessage(client HandlerClient, data []byte) { + var message ClientMessage if err := message.UnmarshalJSON(data); err != nil { if session := client.GetSession(); session != nil { - h.logger.Printf("Error decoding message from client %s: %v", session.PublicId(), err) + log.Printf("Error decoding message from client %s: %v", session.PublicId(), err) session.SendError(InvalidFormat) } else { - h.logger.Printf("Error decoding message from %s: %v", client.RemoteAddr(), err) + log.Printf("Error decoding message from %s: %v", client.RemoteAddr(), err) client.SendError(InvalidFormat) } return @@ -1181,15 +984,15 @@ func (h *Hub) processMessage(client ClientWithSession, data []byte) { if err := message.CheckValid(); err != nil { if session := client.GetSession(); session != nil { - h.logger.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) - if err, ok := err.(*api.Error); ok { + log.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) + if err, ok := err.(*Error); ok { session.SendMessage(message.NewErrorServerMessage(err)) } else { session.SendMessage(message.NewErrorServerMessage(InvalidFormat)) } } else { - h.logger.Printf("Invalid message %+v from %s: %v", message, client.RemoteAddr(), err) - if err, ok := err.(*api.Error); ok { + log.Printf("Invalid message %+v from %s: %v", message, client.RemoteAddr(), err) + if err, ok := err.(*Error); ok { client.SendMessage(message.NewErrorServerMessage(err)) } else { client.SendMessage(message.NewErrorServerMessage(InvalidFormat)) @@ -1237,17 +1040,17 @@ func (h *Hub) processMessage(client ClientWithSession, data []byte) { case "bye": h.processByeMsg(client, &message) case "hello": - h.logger.Printf("Ignore hello %+v for already authenticated connection %s", message.Hello, session.PublicId()) + log.Printf("Ignore hello %+v for already authenticated connection %s", message.Hello, session.PublicId()) default: - h.logger.Printf("Ignore unknown message %+v from %s", message, session.PublicId()) + log.Printf("Ignore unknown message %+v from %s", message, session.PublicId()) } } -func (h *Hub) sendHelloResponse(session *ClientSession, message *api.ClientMessage) bool { - response := &api.ServerMessage{ +func (h *Hub) sendHelloResponse(session *ClientSession, message *ClientMessage) bool { + response := &ServerMessage{ Id: message.Id, Type: "hello", - Hello: &api.HelloServerMessage{ + Hello: &HelloServerMessage{ Version: message.Hello.Version, SessionId: session.PublicId(), ResumeId: session.PrivateId(), @@ -1259,17 +1062,17 @@ func (h *Hub) sendHelloResponse(session *ClientSession, message *api.ClientMessa } type remoteClientInfo struct { - client *grpc.Client - response *grpc.LookupResumeIdReply + client *GrpcClient + response *LookupResumeIdReply } -func (h *Hub) tryProxyResume(c ClientWithSession, resumeId api.PrivateSessionId, message *api.ClientMessage) bool { - client, ok := c.(*HubClient) +func (h *Hub) tryProxyResume(c HandlerClient, resumeId string, message *ClientMessage) bool { + client, ok := c.(*Client) if !ok { return false } - var clients []*grpc.Client + var clients []*GrpcClient if h.rpcClients != nil { clients = h.rpcClients.GetClients() } @@ -1277,7 +1080,7 @@ func (h *Hub) tryProxyResume(c ClientWithSession, resumeId api.PrivateSessionId, return false } - rpcCtx, rpcCancel := context.WithTimeout(client.Context(), 5*time.Second) + rpcCtx, rpcCancel := context.WithTimeout(c.Context(), 5*time.Second) defer rpcCancel() var wg sync.WaitGroup @@ -1285,24 +1088,27 @@ func (h *Hub) tryProxyResume(c ClientWithSession, resumeId api.PrivateSessionId, defer cancel() var remoteClient atomic.Pointer[remoteClientInfo] - for _, grpcClient := range clients { - wg.Go(func() { - if grpcClient.IsSelf() { + for _, c := range clients { + wg.Add(1) + go func(client *GrpcClient) { + defer wg.Done() + + if client.IsSelf() { return } - response, err := grpcClient.LookupResumeId(ctx, resumeId) + response, err := client.LookupResumeId(ctx, resumeId) if err != nil { - h.logger.Printf("Could not lookup resume id %s on %s: %s", resumeId, grpcClient.Target(), err) + log.Printf("Could not lookup resume id %s on %s: %s", resumeId, client.Target(), err) return } cancel() remoteClient.CompareAndSwap(nil, &remoteClientInfo{ - client: grpcClient, + client: client, response: response, }) - }) + }(c) } wg.Wait() @@ -1316,19 +1122,19 @@ func (h *Hub) tryProxyResume(c ClientWithSession, resumeId api.PrivateSessionId, return false } - rs, err := NewRemoteSession(h, client, info.client, api.PublicSessionId(info.response.SessionId)) + rs, err := NewRemoteSession(h, client, info.client, info.response.SessionId) if err != nil { - h.logger.Printf("Could not create remote session %s on %s: %s", info.response.SessionId, info.client.Target(), err) + log.Printf("Could not create remote session %s on %s: %s", info.response.SessionId, info.client.Target(), err) return false } if err := rs.Start(message); err != nil { rs.Close() - h.logger.Printf("Could not start remote session %s on %s: %s", info.response.SessionId, info.client.Target(), err) + log.Printf("Could not start remote session %s on %s: %s", info.response.SessionId, info.client.Target(), err) return false } - h.logger.Printf("Proxy session %s to %s", info.response.SessionId, info.client.Target()) + log.Printf("Proxy session %s to %s", info.response.SessionId, info.client.Target()) h.mu.Lock() defer h.mu.Unlock() h.remoteSessions[rs] = true @@ -1336,16 +1142,16 @@ func (h *Hub) tryProxyResume(c ClientWithSession, resumeId api.PrivateSessionId, return true } -func (h *Hub) processHello(client ClientWithSession, message *api.ClientMessage) { - ctx := log.NewLoggerContext(client.Context(), h.logger) +func (h *Hub) processHello(client HandlerClient, message *ClientMessage) { + ctx := context.TODO() resumeId := message.Hello.ResumeId if resumeId != "" { throttle, err := h.throttler.CheckBruteforce(ctx, client.RemoteAddr(), "HelloResume") - if err == async.ErrBruteforceDetected { + if err == ErrBruteforceDetected { client.SendMessage(message.NewErrorServerMessage(TooManyRequests)) return } else if err != nil { - h.logger.Printf("Error checking for bruteforce: %s", err) + log.Printf("Error checking for bruteforce: %s", err) client.SendMessage(message.NewWrappedErrorServerMessage(err)) return } @@ -1380,7 +1186,7 @@ func (h *Hub) processHello(client ClientWithSession, message *api.ClientMessage) if !ok { // Should never happen as clients only can resume their own sessions. h.mu.Unlock() - h.logger.Printf("Client resumed non-client session %s (private=%s)", session.PublicId(), session.PrivateId()) + log.Printf("Client resumed non-client session %s (private=%s)", session.PublicId(), session.PrivateId()) statsHubSessionResumeFailed.Inc() client.SendMessage(message.NewErrorServerMessage(NoSuchSession)) return @@ -1393,7 +1199,7 @@ func (h *Hub) processHello(client ClientWithSession, message *api.ClientMessage) } if prev := clientSession.SetClient(client); prev != nil { - h.logger.Printf("Closing previous client from %s for session %s", prev.RemoteAddr(), session.PublicId()) + log.Printf("Closing previous client from %s for session %s", prev.RemoteAddr(), session.PublicId()) prev.SendByeResponseWithReason(nil, "session_resumed") } @@ -1402,9 +1208,9 @@ func (h *Hub) processHello(client ClientWithSession, message *api.ClientMessage) delete(h.expectHelloClients, client) h.mu.Unlock() - h.logger.Printf("Resume session from %s in %s (%s) %s (private=%s)", client.RemoteAddr(), client.Country(), client.UserAgent(), session.PublicId(), session.PrivateId()) + log.Printf("Resume session from %s in %s (%s) %s (private=%s)", client.RemoteAddr(), client.Country(), client.UserAgent(), session.PublicId(), session.PrivateId()) - statsHubSessionsResumedTotal.WithLabelValues(clientSession.Backend().Id(), string(clientSession.ClientType())).Inc() + statsHubSessionsResumedTotal.WithLabelValues(clientSession.Backend().Id(), clientSession.ClientType()).Inc() h.sendHelloResponse(clientSession, message) clientSession.NotifySessionResumed(client) return @@ -1416,11 +1222,11 @@ func (h *Hub) processHello(client ClientWithSession, message *api.ClientMessage) h.mu.Unlock() switch message.Hello.Auth.Type { - case api.HelloClientTypeClient: + case HelloClientTypeClient: fallthrough - case api.HelloClientTypeFederation: + case HelloClientTypeFederation: h.processHelloClient(client, message) - case api.HelloClientTypeInternal: + case HelloClientTypeInternal: h.processHelloInternal(client, message) default: h.startExpectHello(client) @@ -1428,21 +1234,19 @@ func (h *Hub) processHello(client ClientWithSession, message *api.ClientMessage) } } -func (h *Hub) processHelloV1(ctx context.Context, client ClientWithSession, message *api.ClientMessage) (*talk.Backend, *talk.BackendClientResponse, error) { - url := message.Hello.Auth.ParsedUrl +func (h *Hub) processHelloV1(ctx context.Context, client HandlerClient, message *ClientMessage) (*Backend, *BackendClientResponse, error) { + url := message.Hello.Auth.parsedUrl backend := h.backend.GetBackend(url) if backend == nil { return nil, nil, InvalidBackendUrl } - url = url.JoinPath(PathToOcsSignalingBackend) - // Run in timeout context to prevent blocking too long. ctx, cancel := context.WithTimeout(ctx, h.backendTimeout) defer cancel() - var auth talk.BackendClientResponse - request := talk.NewBackendClientAuthRequest(message.Hello.Auth.Params) + var auth BackendClientResponse + request := NewBackendClientAuthRequest(message.Hello.Auth.Params) if err := h.backend.PerformJSONRequest(ctx, url, request, &auth); err != nil { return nil, nil, err } @@ -1452,8 +1256,8 @@ func (h *Hub) processHelloV1(ctx context.Context, client ClientWithSession, mess return backend, &auth, nil } -func (h *Hub) processHelloV2(ctx context.Context, client ClientWithSession, message *api.ClientMessage) (*talk.Backend, *talk.BackendClientResponse, error) { - url := message.Hello.Auth.ParsedUrl +func (h *Hub) processHelloV2(ctx context.Context, client HandlerClient, message *ClientMessage) (*Backend, *BackendClientResponse, error) { + url := message.Hello.Auth.parsedUrl backend := h.backend.GetBackend(url) if backend == nil { return nil, nil, InvalidBackendUrl @@ -1462,33 +1266,33 @@ func (h *Hub) processHelloV2(ctx context.Context, client ClientWithSession, mess var tokenString string var tokenClaims jwt.Claims switch message.Hello.Auth.Type { - case api.HelloClientTypeClient: - tokenString = message.Hello.Auth.HelloV2Params.Token - tokenClaims = &api.HelloV2TokenClaims{} - case api.HelloClientTypeFederation: - if !h.backend.HasCapabilityFeature(ctx, url, talk.FeatureFederationV2) { + case HelloClientTypeClient: + tokenString = message.Hello.Auth.helloV2Params.Token + tokenClaims = &HelloV2TokenClaims{} + case HelloClientTypeFederation: + if !h.backend.capabilities.HasCapabilityFeature(ctx, url, FeatureFederationV2) { return nil, nil, ErrFederationNotSupported } - tokenString = message.Hello.Auth.FederationParams.Token - tokenClaims = &api.FederationTokenClaims{} + tokenString = message.Hello.Auth.federationParams.Token + tokenClaims = &FederationTokenClaims{} default: return nil, nil, InvalidClientType } - token, err := jwt.ParseWithClaims(tokenString, tokenClaims, func(token *jwt.Token) (any, error) { + token, err := jwt.ParseWithClaims(tokenString, tokenClaims, func(token *jwt.Token) (interface{}, error) { // Only public-private-key algorithms are supported. - var loadKeyFunc func([]byte) (any, error) + var loadKeyFunc func([]byte) (interface{}, error) switch token.Method.(type) { case *jwt.SigningMethodRSA: - loadKeyFunc = func(data []byte) (any, error) { + loadKeyFunc = func(data []byte) (interface{}, error) { return jwt.ParseRSAPublicKeyFromPEM(data) } case *jwt.SigningMethodECDSA: - loadKeyFunc = func(data []byte) (any, error) { + loadKeyFunc = func(data []byte) (interface{}, error) { return jwt.ParseECPublicKeyFromPEM(data) } case *jwt.SigningMethodEd25519: - loadKeyFunc = func(data []byte) (any, error) { + loadKeyFunc = func(data []byte) (interface{}, error) { if !bytes.HasPrefix(data, []byte("-----BEGIN ")) { // Nextcloud sends the Ed25519 key as base64-encoded public key data. decoded, err := base64.StdEncoding.DecodeString(string(data)) @@ -1510,30 +1314,30 @@ func (h *Hub) processHelloV2(ctx context.Context, client ClientWithSession, mess return jwt.ParseEdPublicKeyFromPEM(data) } default: - h.logger.Printf("Unexpected signing method: %v", token.Header["alg"]) - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + log.Printf("Unexpected signing method: %v", token.Header["alg"]) + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } // Run in timeout context to prevent blocking too long. backendCtx, cancel := context.WithTimeout(ctx, h.backendTimeout) defer cancel() - keyData, cached, found := h.backend.GetStringConfig(backendCtx, url, talk.ConfigGroupSignaling, talk.ConfigKeyHelloV2TokenKey) + keyData, cached, found := h.backend.capabilities.GetStringConfig(backendCtx, url, ConfigGroupSignaling, ConfigKeyHelloV2TokenKey) if !found { if cached { // The Nextcloud instance might just have enabled JWT but we probably use // the cached capabilities without the public key. Make sure to re-fetch. - h.backend.InvalidateCapabilities(url) - keyData, _, found = h.backend.GetStringConfig(backendCtx, url, talk.ConfigGroupSignaling, talk.ConfigKeyHelloV2TokenKey) + h.backend.capabilities.InvalidateCapabilities(url) + keyData, _, found = h.backend.capabilities.GetStringConfig(backendCtx, url, ConfigGroupSignaling, ConfigKeyHelloV2TokenKey) } if !found { - return nil, errors.New("no key found for issuer") + return nil, fmt.Errorf("No key found for issuer") } } key, err := loadKeyFunc([]byte(keyData)) if err != nil { - return nil, fmt.Errorf("could not parse token key: %w", err) + return nil, fmt.Errorf("Could not parse token key: %w", err) } return key, nil @@ -1556,16 +1360,16 @@ func (h *Hub) processHelloV2(ctx context.Context, client ClientWithSession, mess return nil, nil, InvalidToken } - var authTokenClaims api.AuthTokenClaims + var authTokenClaims AuthTokenClaims switch message.Hello.Auth.Type { - case api.HelloClientTypeClient: - claims, ok := token.Claims.(*api.HelloV2TokenClaims) + case HelloClientTypeClient: + claims, ok := token.Claims.(*HelloV2TokenClaims) if !ok || !token.Valid { return nil, nil, InvalidToken } authTokenClaims = claims - case api.HelloClientTypeFederation: - claims, ok := token.Claims.(*api.FederationTokenClaims) + case HelloClientTypeFederation: + claims, ok := token.Claims.(*FederationTokenClaims) if !ok || !token.Valid { return nil, nil, InvalidToken } @@ -1594,9 +1398,9 @@ func (h *Hub) processHelloV2(ctx context.Context, client ClientWithSession, mess return nil, nil, InvalidToken } - auth := &talk.BackendClientResponse{ + auth := &BackendClientResponse{ Type: "auth", - Auth: &talk.BackendClientAuthResponse{ + Auth: &BackendClientAuthResponse{ Version: message.Hello.Version, UserId: subject, User: authTokenClaims.GetUserData(), @@ -1605,27 +1409,27 @@ func (h *Hub) processHelloV2(ctx context.Context, client ClientWithSession, mess return backend, auth, nil } -func (h *Hub) processHelloClient(client ClientWithSession, message *api.ClientMessage) { +func (h *Hub) processHelloClient(client HandlerClient, message *ClientMessage) { // Make sure the client must send another "hello" in case of errors. defer h.startExpectHello(client) - var authFunc func(context.Context, ClientWithSession, *api.ClientMessage) (*talk.Backend, *talk.BackendClientResponse, error) + var authFunc func(context.Context, HandlerClient, *ClientMessage) (*Backend, *BackendClientResponse, error) switch message.Hello.Version { - case api.HelloVersionV1: + case HelloVersionV1: // Auth information contains a ticket that must be validated against the // Nextcloud instance. authFunc = h.processHelloV1 - case api.HelloVersionV2: + case HelloVersionV2: // Auth information contains a JWT that contains all information of the user. authFunc = h.processHelloV2 default: - client.SendMessage(message.NewErrorServerMessage(api.InvalidHelloVersion)) + client.SendMessage(message.NewErrorServerMessage(InvalidHelloVersion)) return } backend, auth, err := authFunc(client.Context(), client, message) if err != nil { - if e, ok := err.(*api.Error); ok { + if e, ok := err.(*Error); ok { client.SendMessage(message.NewErrorServerMessage(e)) } else { client.SendMessage(message.NewWrappedErrorServerMessage(err)) @@ -1636,55 +1440,55 @@ func (h *Hub) processHelloClient(client ClientWithSession, message *api.ClientMe h.processRegister(client, message, backend, auth) } -func (h *Hub) processHelloInternal(client ClientWithSession, message *api.ClientMessage) { +func (h *Hub) processHelloInternal(client HandlerClient, message *ClientMessage) { defer h.startExpectHello(client) if len(h.internalClientsSecret) == 0 { client.SendMessage(message.NewErrorServerMessage(InvalidClientType)) return } - ctx := log.NewLoggerContext(client.Context(), h.logger) + ctx := context.TODO() throttle, err := h.throttler.CheckBruteforce(ctx, client.RemoteAddr(), "HelloInternal") - if err == async.ErrBruteforceDetected { + if err == ErrBruteforceDetected { client.SendMessage(message.NewErrorServerMessage(TooManyRequests)) return } else if err != nil { - h.logger.Printf("Error checking for bruteforce: %s", err) + log.Printf("Error checking for bruteforce: %s", err) client.SendMessage(message.NewWrappedErrorServerMessage(err)) return } // Validate internal connection. - rnd := message.Hello.Auth.InternalParams.Random + rnd := message.Hello.Auth.internalParams.Random mac := hmac.New(sha256.New, h.internalClientsSecret) mac.Write([]byte(rnd)) // nolint check := hex.EncodeToString(mac.Sum(nil)) - if len(rnd) < minTokenRandomLength || check != message.Hello.Auth.InternalParams.Token { + if len(rnd) < minTokenRandomLength || check != message.Hello.Auth.internalParams.Token { throttle(ctx) client.SendMessage(message.NewErrorServerMessage(InvalidToken)) return } - backend := h.backend.GetBackend(message.Hello.Auth.InternalParams.ParsedBackend) + backend := h.backend.GetBackend(message.Hello.Auth.internalParams.parsedBackend) if backend == nil { throttle(ctx) client.SendMessage(message.NewErrorServerMessage(InvalidBackendUrl)) return } - auth := &talk.BackendClientResponse{ + auth := &BackendClientResponse{ Type: "auth", - Auth: &talk.BackendClientAuthResponse{}, + Auth: &BackendClientAuthResponse{}, } h.processRegister(client, message, backend, auth) } -func (h *Hub) disconnectByRoomSessionId(ctx context.Context, roomSessionId api.RoomSessionId, backend *talk.Backend) { +func (h *Hub) disconnectByRoomSessionId(ctx context.Context, roomSessionId string, backend *Backend) { sessionId, err := h.roomSessions.LookupSessionId(ctx, roomSessionId, "room_session_reconnected") if err == ErrNoSuchRoomSession { return } else if err != nil { - h.logger.Printf("Could not get session id for room session %s: %s", roomSessionId, err) + log.Printf("Could not get session id for room session %s: %s", roomSessionId, err) return } @@ -1692,104 +1496,53 @@ func (h *Hub) disconnectByRoomSessionId(ctx context.Context, roomSessionId api.R if session == nil { // Session is located on a different server. Should already have been closed // but send "bye" again as additional safeguard. - msg := &events.AsyncMessage{ + msg := &AsyncMessage{ Type: "message", - Message: &api.ServerMessage{ + Message: &ServerMessage{ Type: "bye", - Bye: &api.ByeServerMessage{ + Bye: &ByeServerMessage{ Reason: "room_session_reconnected", }, }, } if err := h.events.PublishSessionMessage(sessionId, backend, msg); err != nil { - h.logger.Printf("Could not send reconnect bye to session %s: %s", sessionId, err) + log.Printf("Could not send reconnect bye to session %s: %s", sessionId, err) } return } - h.logger.Printf("Closing session %s because same room session %s connected", session.PublicId(), roomSessionId) - h.disconnectSessionWithReason(session, "room_session_reconnected") -} - -func (h *Hub) DisconnectSessionByRoomSessionId(sessionId api.PublicSessionId, roomSessionId api.RoomSessionId, reason string) { - session := h.GetSessionByPublicId(sessionId) - if session == nil { - return - } - - h.logger.Printf("Closing session %s because same room session %s connected", session.PublicId(), roomSessionId) - h.disconnectSessionWithReason(session, reason) -} - -func (h *Hub) disconnectSessionWithReason(session Session, reason string) { + log.Printf("Closing session %s because same room session %s connected", session.PublicId(), roomSessionId) session.LeaveRoom(false) switch sess := session.(type) { case *ClientSession: if client := sess.GetClient(); client != nil { - client.SendByeResponseWithReason(nil, reason) + client.SendByeResponseWithReason(nil, "room_session_reconnected") } } session.Close() } -func (h *Hub) sendRoom(session *ClientSession, message *api.ClientMessage, room *Room) bool { - response := &api.ServerMessage{ +func (h *Hub) sendRoom(session *ClientSession, message *ClientMessage, room *Room) bool { + response := &ServerMessage{ Type: "room", } if message != nil { response.Id = message.Id } if room == nil { - response.Room = &api.RoomServerMessage{ + response.Room = &RoomServerMessage{ RoomId: "", } } else { - response.Room = &api.RoomServerMessage{ + response.Room = &RoomServerMessage{ RoomId: room.id, - Properties: room.Properties(), - } - var mcuStreamBitrate api.Bandwidth - var mcuScreenBitrate api.Bandwidth - if mcu := h.mcu; mcu != nil { - mcuStreamBitrate, mcuScreenBitrate = mcu.GetBandwidthLimits() - } - - var backendStreamBitrate api.Bandwidth - var backendScreenBitrate api.Bandwidth - if backend := room.Backend(); backend != nil { - backendStreamBitrate = backend.MaxStreamBitrate() - backendScreenBitrate = backend.MaxScreenBitrate() - } - - var maxStreamBitrate api.Bandwidth - if mcuStreamBitrate != 0 && backendStreamBitrate != 0 { - maxStreamBitrate = min(mcuStreamBitrate, backendStreamBitrate) - } else if mcuStreamBitrate != 0 { - maxStreamBitrate = mcuStreamBitrate - } else { - maxStreamBitrate = backendStreamBitrate - } - - var maxScreenBitrate api.Bandwidth - if mcuScreenBitrate != 0 && backendScreenBitrate != 0 { - maxScreenBitrate = min(mcuScreenBitrate, backendScreenBitrate) - } else if mcuScreenBitrate != 0 { - maxScreenBitrate = mcuScreenBitrate - } else { - maxScreenBitrate = backendScreenBitrate - } - - if maxStreamBitrate != 0 || maxScreenBitrate != 0 { - response.Room.Bandwidth = &api.RoomBandwidth{ - MaxStreamBitrate: maxStreamBitrate, - MaxScreenBitrate: maxScreenBitrate, - } + Properties: room.properties, } } return session.SendMessage(response) } -func (h *Hub) processRoom(sess Session, message *api.ClientMessage) { +func (h *Hub) processRoom(sess Session, message *ClientMessage) { session, ok := sess.(*ClientSession) if !ok { return @@ -1799,11 +1552,11 @@ func (h *Hub) processRoom(sess Session, message *api.ClientMessage) { if roomId == "" { // We can handle leaving a room directly. if session.LeaveRoomWithMessage(true, message) != nil { - if session.UserId() == "" && session.ClientType() != api.HelloClientTypeInternal { - h.startWaitAnonymousSessionRoom(session) - } // User was in a room before, so need to notify about leaving it. h.sendRoom(session, message, nil) + if session.UserId() == "" && session.ClientType() != HelloClientTypeInternal { + h.startWaitAnonymousSessionRoom(session) + } } return @@ -1838,37 +1591,45 @@ func (h *Hub) processRoom(sess Session, message *api.ClientMessage) { if session.UserId() == "" && client == nil { h.startWaitAnonymousSessionRoom(session) } - if ae, ok := internal.AsErrorType[*api.Error](err); ok { + var ae *Error + if errors.As(err, &ae) { session.SendMessage(message.NewErrorServerMessage(ae)) return } - var details any - if ce, ok := internal.AsErrorType[*tls.CertificateVerificationError](err); ok { + var details interface{} + var ce *tls.CertificateVerificationError + if errors.As(err, &ce) { details = map[string]string{ "code": "certificate_verification_error", "message": ce.Error(), } - } else if ne, ok := internal.AsErrorType[net.Error](err); ok { + } + var ne net.Error + if details == nil && errors.As(err, &ne) { details = map[string]string{ "code": "network_error", "message": ne.Error(), } - } else if errors.Is(err, websocket.ErrBadHandshake) { - details = map[string]string{ - "code": "network_error", - "message": err.Error(), - } - } else if we, ok := internal.AsErrorType[websocket.HandshakeError](err); ok { - details = map[string]string{ - "code": "network_error", - "message": we.Error(), + } + if details == nil { + var we websocket.HandshakeError + if errors.Is(err, websocket.ErrBadHandshake) { + details = map[string]string{ + "code": "network_error", + "message": err.Error(), + } + } else if errors.As(err, &we) { + details = map[string]string{ + "code": "network_error", + "message": we.Error(), + } } } - h.logger.Printf("Error creating federation client to %s for %s to join room %s: %s", federation.SignalingUrl, session.PublicId(), roomId, err) + log.Printf("Error creating federation client to %s for %s to join room %s: %s", federation.SignalingUrl, session.PublicId(), roomId, err) session.SendMessage(message.NewErrorServerMessage( - api.NewErrorDetail("federation_error", "Failed to create federation client.", details), + NewErrorDetail("federation_error", "Failed to create federation client.", details), )) return } @@ -1878,20 +1639,19 @@ func (h *Hub) processRoom(sess Session, message *api.ClientMessage) { roomSessionId := message.Room.SessionId if roomSessionId == "" { // TODO(jojo): Better make the session id required in the request. - h.logger.Printf("User did not send a room session id, assuming session %s", session.PublicId()) - roomSessionId = api.RoomSessionId(session.PublicId()) + log.Printf("User did not send a room session id, assuming session %s", session.PublicId()) + roomSessionId = session.PublicId() } // Prefix room session id to allow using the same signaling server for two Nextcloud instances during development. // Otherwise the same room session id will be detected and the other session will be kicked. - if err := session.UpdateRoomSessionId(api.FederatedRoomSessionIdPrefix + roomSessionId); err != nil { - h.logger.Printf("Error updating room session id for session %s: %s", session.PublicId(), err) + if err := session.UpdateRoomSessionId(FederatedRoomSessionIdPrefix + roomSessionId); err != nil { + log.Printf("Error updating room session id for session %s: %s", session.PublicId(), err) } h.mu.Lock() - defer h.mu.Unlock() h.federatedSessions[session] = true - h.federationClients[client] = true + h.mu.Unlock() return } @@ -1900,32 +1660,30 @@ func (h *Hub) processRoom(sess Session, message *api.ClientMessage) { roomSessionId := message.Room.SessionId if roomSessionId == "" { // TODO(jojo): Better make the session id required in the request. - h.logger.Printf("User did not send a room session id, assuming session %s", session.PublicId()) - roomSessionId = api.RoomSessionId(session.PublicId()) + log.Printf("User did not send a room session id, assuming session %s", session.PublicId()) + roomSessionId = session.PublicId() } if err := session.UpdateRoomSessionId(roomSessionId); err != nil { - h.logger.Printf("Error updating room session id for session %s: %s", session.PublicId(), err) + log.Printf("Error updating room session id for session %s: %s", session.PublicId(), err) } session.SendMessage(message.NewErrorServerMessage( - api.NewErrorDetail("already_joined", "Already joined this room.", &api.RoomErrorDetails{ - Room: &api.RoomServerMessage{ + NewErrorDetail("already_joined", "Already joined this room.", &RoomErrorDetails{ + Room: &RoomServerMessage{ RoomId: room.id, - Properties: room.Properties(), + Properties: room.properties, }, }), )) return } - var room talk.BackendClientResponse - var joinRoomTime time.Time - if session.ClientType() == api.HelloClientTypeInternal { + var room BackendClientResponse + if session.ClientType() == HelloClientTypeInternal { // Internal clients can join any room. - joinRoomTime = time.Now() - room = talk.BackendClientResponse{ + room = BackendClientResponse{ Type: "room", - Room: &talk.BackendClientRoomResponse{ + Room: &BackendClientRoomResponse{ RoomId: roomId, }, } @@ -1937,19 +1695,18 @@ func (h *Hub) processRoom(sess Session, message *api.ClientMessage) { sessionId := message.Room.SessionId if sessionId == "" { // TODO(jojo): Better make the session id required in the request. - h.logger.Printf("User did not send a room session id, assuming session %s", session.PublicId()) - sessionId = api.RoomSessionId(session.PublicId()) + log.Printf("User did not send a room session id, assuming session %s", session.PublicId()) + sessionId = session.PublicId() } - request := talk.NewBackendClientRoomRequest(roomId, session.UserId(), sessionId) + request := NewBackendClientRoomRequest(roomId, session.UserId(), sessionId) request.Room.UpdateFromSession(session) - if err := h.backend.PerformJSONRequest(ctx, session.ParsedBackendOcsUrl(), request, &room); err != nil { + if err := h.backend.PerformJSONRequest(ctx, session.ParsedBackendUrl(), request, &room); err != nil { session.SendMessage(message.NewWrappedErrorServerMessage(err)) return } // TODO(jojo): Validate response - joinRoomTime = time.Now() if message.Room.SessionId != "" { // There can only be one connection per Nextcloud Talk session, // disconnect any other connections without sending a "leave" event. @@ -1960,7 +1717,7 @@ func (h *Hub) processRoom(sess Session, message *api.ClientMessage) { } } - h.processJoinRoom(session, message, &room, joinRoomTime) + h.processJoinRoom(session, message, &room) } func (h *Hub) publishFederatedSessions() (int, *sync.WaitGroup) { @@ -1972,7 +1729,7 @@ func (h *Hub) publishFederatedSessions() (int, *sync.WaitGroup) { return 0, &wg } - rooms := make(map[string]map[string][]talk.BackendPingEntry) + rooms := make(map[string]map[string][]BackendPingEntry) urls := make(map[string]*url.URL) for session := range h.federatedSessions { u := session.BackendUrl() @@ -1985,24 +1742,25 @@ func (h *Hub) publishFederatedSessions() (int, *sync.WaitGroup) { continue } - var sid api.RoomSessionId + var sid string var uid string // Use Nextcloud session id and user id - if sid = session.RoomSessionId().WithoutFederation(); sid == "" { + sid = strings.TrimPrefix(session.RoomSessionId(), FederatedRoomSessionIdPrefix) + uid = session.AuthUserId() + if sid == "" { continue } - uid = session.AuthUserId() roomId := federation.RoomId() entries, found := rooms[roomId] if !found { - entries = make(map[string][]talk.BackendPingEntry) + entries = make(map[string][]BackendPingEntry) rooms[roomId] = entries } e, found := entries[u] if !found { - p := session.ParsedBackendOcsUrl() + p := session.ParsedBackendUrl() if p == nil { // Should not happen, invalid URLs should get rejected earlier. continue @@ -2010,7 +1768,7 @@ func (h *Hub) publishFederatedSessions() (int, *sync.WaitGroup) { urls[u] = p } - entries[u] = append(e, talk.BackendPingEntry{ + entries[u] = append(e, BackendPingEntry{ SessionId: sid, UserId: uid, }) @@ -2020,18 +1778,17 @@ func (h *Hub) publishFederatedSessions() (int, *sync.WaitGroup) { return 0, &wg } count := 0 - ctx := log.NewLoggerContext(context.Background(), h.logger) for roomId, entries := range rooms { for u, e := range entries { wg.Add(1) count += len(e) - go func(roomId string, url *url.URL, entries []talk.BackendPingEntry) { + go func(roomId string, url *url.URL, entries []BackendPingEntry) { defer wg.Done() - sendCtx, cancel := context.WithTimeout(ctx, h.backendTimeout) + ctx, cancel := context.WithTimeout(context.Background(), h.backendTimeout) defer cancel() - if err := h.roomPing.SendPings(sendCtx, roomId, url, entries); err != nil { - h.logger.Printf("Error pinging room %s for active entries %+v: %s", roomId, entries, err) + if err := h.roomPing.SendPings(ctx, roomId, url, entries); err != nil { + log.Printf("Error pinging room %s for active entries %+v: %s", roomId, entries, err) } }(roomId, urls[u], e) } @@ -2039,7 +1796,7 @@ func (h *Hub) publishFederatedSessions() (int, *sync.WaitGroup) { return count, &wg } -func (h *Hub) GetRoomForBackend(id string, backend *talk.Backend) *Room { +func (h *Hub) GetRoomForBackend(id string, backend *Backend) *Room { internalRoomId := getRoomIdForBackend(id, backend) h.ru.RLock() @@ -2047,45 +1804,6 @@ func (h *Hub) GetRoomForBackend(id string, backend *talk.Backend) *Room { return h.rooms[internalRoomId] } -func (h *Hub) GetInternalSessions(roomId string, backend *talk.Backend) ([]*grpc.InternalSessionData, []*grpc.VirtualSessionData, bool) { - room := h.GetRoomForBackend(roomId, backend) - if room == nil { - return nil, nil, false - } - - room.mu.RLock() - defer room.mu.RUnlock() - - var internalSessions []*grpc.InternalSessionData - var virtualSessions []*grpc.VirtualSessionData - for session := range room.internalSessions { - internalSessions = append(internalSessions, &grpc.InternalSessionData{ - SessionId: string(session.PublicId()), - InCall: uint32(session.GetInCall()), - Features: session.GetFeatures(), - }) - } - - for session := range room.virtualSessions { - virtualSessions = append(virtualSessions, &grpc.VirtualSessionData{ - SessionId: string(session.PublicId()), - InCall: uint32(session.GetInCall()), - }) - } - - return internalSessions, virtualSessions, true -} - -func (h *Hub) GetTransientEntries(roomId string, backend *talk.Backend) (api.TransientDataEntries, bool) { - room := h.GetRoomForBackend(roomId, backend) - if room == nil { - return nil, false - } - - entries := room.transientData.GetEntries() - return entries, true -} - func (h *Hub) removeRoom(room *Room) { internalRoomId := getRoomIdForBackend(room.Id(), room.Backend()) h.ru.Lock() @@ -2097,15 +1815,7 @@ func (h *Hub) removeRoom(room *Room) { h.roomPing.DeleteRoom(room.Id()) } -func (h *Hub) CreateRoom(id string, properties json.RawMessage, backend *talk.Backend) (*Room, error) { - h.ru.Lock() - defer h.ru.Unlock() - - return h.createRoomLocked(id, properties, backend) -} - -// +checklocks:h.ru -func (h *Hub) createRoomLocked(id string, properties json.RawMessage, backend *talk.Backend) (*Room, error) { +func (h *Hub) createRoom(id string, properties json.RawMessage, backend *Backend) (*Room, error) { // Note the write lock must be held. room, err := NewRoom(id, properties, h, h.events, backend) if err != nil { @@ -2118,7 +1828,7 @@ func (h *Hub) createRoomLocked(id string, properties json.RawMessage, backend *t return room, nil } -func (h *Hub) processJoinRoom(session *ClientSession, message *api.ClientMessage, room *talk.BackendClientResponse, joinTime time.Time) { +func (h *Hub) processJoinRoom(session *ClientSession, message *ClientMessage, room *BackendClientResponse) { if room.Type == "error" { session.SendMessage(message.NewErrorServerMessage(room.Error)) return @@ -2145,7 +1855,7 @@ func (h *Hub) processJoinRoom(session *ClientSession, message *api.ClientMessage r, found := h.rooms[internalRoomId] if !found { var err error - if r, err = h.createRoomLocked(roomId, room.Room.Properties, session.Backend()); err != nil { + if r, err = h.createRoom(roomId, room.Room.Properties, session.Backend()); err != nil { h.ru.Unlock() session.SendMessage(message.NewWrappedErrorServerMessage(err)) // The session (implicitly) left the room due to an error. @@ -2159,12 +1869,12 @@ func (h *Hub) processJoinRoom(session *ClientSession, message *api.ClientMessage h.mu.Lock() // The session now joined a room, don't expire if it is anonymous. delete(h.anonymousSessions, session) - if session.ClientType() == api.HelloClientTypeInternal && session.HasFeature(api.ClientFeatureStartDialout) { + if session.ClientType() == HelloClientTypeInternal && session.HasFeature(ClientFeatureStartDialout) { // An internal session in a room can not be used for dialout. delete(h.dialoutSessions, session) } h.mu.Unlock() - session.SetRoom(r, joinTime) + session.SetRoom(r) if room.Room.Permissions != nil { session.SetPermissions(*room.Room.Permissions) } @@ -2172,7 +1882,7 @@ func (h *Hub) processJoinRoom(session *ClientSession, message *api.ClientMessage r.AddSession(session, room.Room.Session) } -func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { +func (h *Hub) processMessageMsg(sess Session, message *ClientMessage) { session, ok := sess.(*ClientSession) if !ok { // Client is not connected yet. @@ -2182,19 +1892,19 @@ func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { msg := message.Message var recipient *ClientSession var subject string - var clientData *api.MessageClientMessageData - var serverRecipient *api.MessageClientMessageRecipient - var recipientSessionId api.PublicSessionId + var clientData *MessageClientMessageData + var serverRecipient *MessageClientMessageRecipient + var recipientSessionId string var room *Room switch msg.Recipient.Type { - case api.RecipientTypeSession: + case RecipientTypeSession: if h.mcu != nil { // Maybe this is a message to be processed by the MCU. - var data api.MessageClientMessageData - if err := data.UnmarshalJSON(msg.Data); err == nil { + var data MessageClientMessageData + if err := json.Unmarshal(msg.Data, &data); err == nil { if err := data.CheckValid(); err != nil { - h.logger.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) - if err, ok := err.(*api.Error); ok { + log.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) + if err, ok := err.(*Error); ok { session.SendMessage(message.NewErrorServerMessage(err)) } else { session.SendMessage(message.NewErrorServerMessage(InvalidFormat)) @@ -2236,12 +1946,12 @@ func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { return } - publisher := session.GetPublisher(sfu.StreamTypeScreen) + publisher := session.GetPublisher(StreamTypeScreen) if publisher == nil { return } - h.logger.Printf("Closing screen publisher for %s", session.PublicId()) + log.Printf("Closing screen publisher for %s", session.PublicId()) ctx, cancel := context.WithTimeout(context.Background(), h.mcuTimeout) defer cancel() publisher.Close(ctx) @@ -2264,31 +1974,31 @@ func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { return } - subject = events.GetSubjectForSessionId(msg.Recipient.SessionId, sess.Backend()) + subject = "session." + msg.Recipient.SessionId recipientSessionId = msg.Recipient.SessionId if sess, ok := sess.(*ClientSession); ok { recipient = sess } // Send to client connection for virtual sessions. - if sess.ClientType() == api.HelloClientTypeVirtual { + if sess.ClientType() == HelloClientTypeVirtual { virtualSession := sess.(*VirtualSession) clientSession := virtualSession.Session() - subject = events.GetSubjectForSessionId(clientSession.PublicId(), sess.Backend()) + subject = "session." + clientSession.PublicId() recipientSessionId = clientSession.PublicId() recipient = clientSession // The client should see his session id as recipient. - serverRecipient = &api.MessageClientMessageRecipient{ + serverRecipient = &MessageClientMessageRecipient{ Type: "session", SessionId: virtualSession.SessionId(), } } } else { - subject = events.GetSubjectForSessionId(msg.Recipient.SessionId, nil) + subject = "session." + msg.Recipient.SessionId recipientSessionId = msg.Recipient.SessionId serverRecipient = &msg.Recipient } - case api.RecipientTypeUser: + case RecipientTypeUser: if msg.Recipient.UserId != "" { if msg.Recipient.UserId == session.UserId() { // Don't loop messages to the sender. @@ -2297,21 +2007,21 @@ func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { return } - subject = events.GetSubjectForUserId(msg.Recipient.UserId, session.Backend()) + subject = GetSubjectForUserId(msg.Recipient.UserId, session.Backend()) } - case api.RecipientTypeRoom: + case RecipientTypeRoom: fallthrough - case api.RecipientTypeCall: + case RecipientTypeCall: if session != nil { if room = session.GetRoom(); room != nil { - subject = events.GetSubjectForRoomId(room.Id(), room.Backend()) + subject = GetSubjectForRoomId(room.Id(), room.Backend()) if h.mcu != nil { - var data api.MessageClientMessageData - if err := data.UnmarshalJSON(msg.Data); err == nil { + var data MessageClientMessageData + if err := json.Unmarshal(msg.Data, &data); err == nil { if err := data.CheckValid(); err != nil { - h.logger.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) - if err, ok := err.(*api.Error); ok { + log.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) + if err, ok := err.(*Error); ok { session.SendMessage(message.NewErrorServerMessage(err)) } else { session.SendMessage(message.NewErrorServerMessage(InvalidFormat)) @@ -2326,14 +2036,14 @@ func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { } } if subject == "" { - h.logger.Printf("Unknown recipient in message %+v from %s", msg, session.PublicId()) + log.Printf("Unknown recipient in message %+v from %s", msg, session.PublicId()) return } - response := &api.ServerMessage{ + response := &ServerMessage{ Type: "message", - Message: &api.MessageServerMessage{ - Sender: &api.MessageServerMessageSender{ + Message: &MessageServerMessage{ + Sender: &MessageServerMessageSender{ Type: msg.Recipient.Type, SessionId: session.PublicId(), UserId: session.UserId(), @@ -2346,7 +2056,7 @@ func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { // The recipient is connected to this instance, no need to go through asynchronous events. if clientData != nil && clientData.Type == "sendoffer" { if err := session.IsAllowedToSend(clientData); err != nil { - h.logger.Printf("Session %s is not allowed to send offer for %s, ignoring (%s)", session.PublicId(), clientData.RoomType, err) + log.Printf("Session %s is not allowed to send offer for %s, ignoring (%s)", session.PublicId(), clientData.RoomType, err) sendNotAllowed(session, message, "Not allowed to send offer") return } @@ -2358,20 +2068,20 @@ func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { ctx, cancel := context.WithTimeout(session.Context(), h.mcuTimeout) defer cancel() - mc, err := recipient.GetOrCreateSubscriber(ctx, h.mcu, session.PublicId(), sfu.StreamType(clientData.RoomType)) + mc, err := recipient.GetOrCreateSubscriber(ctx, h.mcu, session.PublicId(), StreamType(clientData.RoomType)) if err != nil { - h.logger.Printf("Could not create MCU subscriber for session %s to send %+v to %s: %s", session.PublicId(), clientData, recipient.PublicId(), err) + log.Printf("Could not create MCU subscriber for session %s to send %+v to %s: %s", session.PublicId(), clientData, recipient.PublicId(), err) sendMcuClientNotFound(session, message) return } else if mc == nil { - h.logger.Printf("No MCU subscriber found for session %s to send %+v to %s", session.PublicId(), clientData, recipient.PublicId()) + log.Printf("No MCU subscriber found for session %s to send %+v to %s", session.PublicId(), clientData, recipient.PublicId()) sendMcuClientNotFound(session, message) return } - mc.SendMessage(session.Context(), msg, clientData, func(err error, response api.StringMap) { + mc.SendMessage(session.Context(), msg, clientData, func(err error, response map[string]interface{}) { if err != nil { - h.logger.Printf("Could not send MCU message %+v for session %s to %s: %s", clientData, session.PublicId(), recipient.PublicId(), err) + log.Printf("Could not send MCU message %+v for session %s to %s: %s", clientData, session.PublicId(), recipient.PublicId(), err) sendMcuProcessingFailed(session, message) return } else if response == nil { @@ -2392,56 +2102,56 @@ func (h *Hub) processMessageMsg(sess Session, message *api.ClientMessage) { } else { if clientData != nil && clientData.Type == "sendoffer" { if err := session.IsAllowedToSend(clientData); err != nil { - h.logger.Printf("Session %s is not allowed to send offer for %s, ignoring (%s)", session.PublicId(), clientData.RoomType, err) + log.Printf("Session %s is not allowed to send offer for %s, ignoring (%s)", session.PublicId(), clientData.RoomType, err) sendNotAllowed(session, message, "Not allowed to send offer") return } - async := &events.AsyncMessage{ + async := &AsyncMessage{ Type: "sendoffer", - SendOffer: &events.SendOfferMessage{ + SendOffer: &SendOfferMessage{ MessageId: message.Id, SessionId: session.PublicId(), Data: clientData, }, } if err := h.events.PublishSessionMessage(recipientSessionId, session.Backend(), async); err != nil { - h.logger.Printf("Error publishing message to remote session: %s", err) + log.Printf("Error publishing message to remote session: %s", err) } return } - async := &events.AsyncMessage{ + async := &AsyncMessage{ Type: "message", Message: response, } var err error switch msg.Recipient.Type { - case api.RecipientTypeSession: + case RecipientTypeSession: err = h.events.PublishSessionMessage(recipientSessionId, session.Backend(), async) - case api.RecipientTypeUser: + case RecipientTypeUser: err = h.events.PublishUserMessage(msg.Recipient.UserId, session.Backend(), async) - case api.RecipientTypeRoom: + case RecipientTypeRoom: fallthrough - case api.RecipientTypeCall: + case RecipientTypeCall: err = h.events.PublishRoomMessage(room.Id(), session.Backend(), async) default: err = fmt.Errorf("unsupported recipient type: %s", msg.Recipient.Type) } if err != nil { - h.logger.Printf("Error publishing message to remote session: %s", err) + log.Printf("Error publishing message to remote session: %s", err) } } } func isAllowedToControl(session Session) bool { - if session.ClientType() == api.HelloClientTypeInternal { + if session.ClientType() == HelloClientTypeInternal { // Internal clients are allowed to send any control message. return true } - if session.HasPermission(api.PERMISSION_MAY_CONTROL) { + if session.HasPermission(PERMISSION_MAY_CONTROL) { // Moderator clients are allowed to send any control message. return true } @@ -2449,20 +2159,20 @@ func isAllowedToControl(session Session) bool { return false } -func (h *Hub) processControlMsg(session Session, message *api.ClientMessage) { +func (h *Hub) processControlMsg(session Session, message *ClientMessage) { msg := message.Control if !isAllowedToControl(session) { - h.logger.Printf("Ignore control message %+v from %s", msg, session.PublicId()) + log.Printf("Ignore control message %+v from %s", msg, session.PublicId()) return } var recipient *ClientSession var subject string - var serverRecipient *api.MessageClientMessageRecipient - var recipientSessionId api.PublicSessionId + var serverRecipient *MessageClientMessageRecipient + var recipientSessionId string var room *Room switch msg.Recipient.Type { - case api.RecipientTypeSession: + case RecipientTypeSession: data := h.decodePublicSessionId(msg.Recipient.SessionId) if data != nil { if msg.Recipient.SessionId == session.PublicId() { @@ -2470,7 +2180,7 @@ func (h *Hub) processControlMsg(session Session, message *api.ClientMessage) { return } - subject = events.GetSubjectForSessionId(msg.Recipient.SessionId, nil) + subject = "session." + msg.Recipient.SessionId recipientSessionId = msg.Recipient.SessionId h.mu.RLock() sess, found := h.sessions[data.Sid] @@ -2480,14 +2190,14 @@ func (h *Hub) processControlMsg(session Session, message *api.ClientMessage) { } // Send to client connection for virtual sessions. - if sess.ClientType() == api.HelloClientTypeVirtual { + if sess.ClientType() == HelloClientTypeVirtual { virtualSession := sess.(*VirtualSession) clientSession := virtualSession.Session() - subject = events.GetSubjectForSessionId(clientSession.PublicId(), sess.Backend()) + subject = "session." + clientSession.PublicId() recipientSessionId = clientSession.PublicId() recipient = clientSession // The client should see his session id as recipient. - serverRecipient = &api.MessageClientMessageRecipient{ + serverRecipient = &MessageClientMessageRecipient{ Type: "session", SessionId: virtualSession.SessionId(), } @@ -2499,7 +2209,7 @@ func (h *Hub) processControlMsg(session Session, message *api.ClientMessage) { } else { serverRecipient = &msg.Recipient } - case api.RecipientTypeUser: + case RecipientTypeUser: if msg.Recipient.UserId != "" { if msg.Recipient.UserId == session.UserId() { // Don't loop messages to the sender. @@ -2508,26 +2218,26 @@ func (h *Hub) processControlMsg(session Session, message *api.ClientMessage) { return } - subject = events.GetSubjectForUserId(msg.Recipient.UserId, session.Backend()) + subject = GetSubjectForUserId(msg.Recipient.UserId, session.Backend()) } - case api.RecipientTypeRoom: + case RecipientTypeRoom: fallthrough - case api.RecipientTypeCall: + case RecipientTypeCall: if session != nil { if room = session.GetRoom(); room != nil { - subject = events.GetSubjectForRoomId(room.Id(), room.Backend()) + subject = GetSubjectForRoomId(room.Id(), room.Backend()) } } } if subject == "" { - h.logger.Printf("Unknown recipient in message %+v from %s", msg, session.PublicId()) + log.Printf("Unknown recipient in message %+v from %s", msg, session.PublicId()) return } - response := &api.ServerMessage{ + response := &ServerMessage{ Type: "control", - Control: &api.ControlServerMessage{ - Sender: &api.MessageServerMessageSender{ + Control: &ControlServerMessage{ + Sender: &MessageServerMessageSender{ Type: msg.Recipient.Type, SessionId: session.PublicId(), UserId: session.UserId(), @@ -2539,37 +2249,37 @@ func (h *Hub) processControlMsg(session Session, message *api.ClientMessage) { if recipient != nil { recipient.SendMessage(response) } else { - async := &events.AsyncMessage{ + async := &AsyncMessage{ Type: "message", Message: response, } var err error switch msg.Recipient.Type { - case api.RecipientTypeSession: + case RecipientTypeSession: err = h.events.PublishSessionMessage(recipientSessionId, session.Backend(), async) - case api.RecipientTypeUser: + case RecipientTypeUser: err = h.events.PublishUserMessage(msg.Recipient.UserId, session.Backend(), async) - case api.RecipientTypeRoom: + case RecipientTypeRoom: fallthrough - case api.RecipientTypeCall: + case RecipientTypeCall: err = h.events.PublishRoomMessage(room.Id(), room.Backend(), async) default: err = fmt.Errorf("unsupported recipient type: %s", msg.Recipient.Type) } if err != nil { - h.logger.Printf("Error publishing message to remote session: %s", err) + log.Printf("Error publishing message to remote session: %s", err) } } } -func (h *Hub) processInternalMsg(sess Session, message *api.ClientMessage) { +func (h *Hub) processInternalMsg(sess Session, message *ClientMessage) { msg := message.Internal session, ok := sess.(*ClientSession) if !ok { // Client is not connected yet. return - } else if session.ClientType() != api.HelloClientTypeInternal { - h.logger.Printf("Ignore internal message %+v from %s", msg, session.PublicId()) + } else if session.ClientType() != HelloClientTypeInternal { + log.Printf("Ignore internal message %+v from %s", msg, session.PublicId()) return } @@ -2582,19 +2292,19 @@ func (h *Hub) processInternalMsg(sess Session, message *api.ClientMessage) { msg := msg.AddSession room := h.GetRoomForBackend(msg.RoomId, session.Backend()) if room == nil { - h.logger.Printf("Ignore add session message %+v for invalid room %s from %s", *msg, msg.RoomId, session.PublicId()) + log.Printf("Ignore add session message %+v for invalid room %s from %s", *msg, msg.RoomId, session.PublicId()) return } sessionIdData := h.newSessionIdData(session.Backend()) - privateSessionId, err := h.sessionIds.EncodePrivate(sessionIdData) + privateSessionId, err := h.cookie.EncodePrivate(sessionIdData) if err != nil { - h.logger.Printf("Could not encode private virtual session id: %s", err) + log.Printf("Could not encode private virtual session id: %s", err) return } - publicSessionId, err := h.sessionIds.EncodePublic(sessionIdData) + publicSessionId, err := h.cookie.EncodePublic(sessionIdData) if err != nil { - h.logger.Printf("Could not encode public virtual session id: %s", err) + log.Printf("Could not encode public virtual session id: %s", err) return } @@ -2605,41 +2315,41 @@ func (h *Hub) processInternalMsg(sess Session, message *api.ClientMessage) { sess, err := NewVirtualSession(session, privateSessionId, publicSessionId, sessionIdData, msg) if err != nil { - h.logger.Printf("Could not create virtual session %s: %s", virtualSessionId, err) - reply := message.NewErrorServerMessage(api.NewError("add_failed", "Could not create virtual session.")) + log.Printf("Could not create virtual session %s: %s", virtualSessionId, err) + reply := message.NewErrorServerMessage(NewError("add_failed", "Could not create virtual session.")) session.SendMessage(reply) return } - if options := msg.Options; options != nil && options.ActorId != "" && options.ActorType != "" { - request := talk.NewBackendClientRoomRequest(room.Id(), msg.UserId, api.RoomSessionId(publicSessionId)) - request.Room.ActorId = options.ActorId - request.Room.ActorType = options.ActorType + if msg.Options != nil { + request := NewBackendClientRoomRequest(room.Id(), msg.UserId, publicSessionId) + request.Room.ActorId = msg.Options.ActorId + request.Room.ActorType = msg.Options.ActorType request.Room.InCall = sess.GetInCall() - var response talk.BackendClientResponse - if err := h.backend.PerformJSONRequest(ctx, session.ParsedBackendOcsUrl(), request, &response); err != nil { + var response BackendClientResponse + if err := h.backend.PerformJSONRequest(ctx, session.ParsedBackendUrl(), request, &response); err != nil { sess.Close() - h.logger.Printf("Could not join virtual session %s at backend %s: %s", virtualSessionId, session.BackendUrl(), err) - reply := message.NewErrorServerMessage(api.NewError("add_failed", "Could not join virtual session.")) + log.Printf("Could not join virtual session %s at backend %s: %s", virtualSessionId, session.BackendUrl(), err) + reply := message.NewErrorServerMessage(NewError("add_failed", "Could not join virtual session.")) session.SendMessage(reply) return } if response.Type == "error" { sess.Close() - h.logger.Printf("Could not join virtual session %s at backend %s: %+v", virtualSessionId, session.BackendUrl(), response.Error) - reply := message.NewErrorServerMessage(api.NewError("add_failed", response.Error.Error())) + log.Printf("Could not join virtual session %s at backend %s: %+v", virtualSessionId, session.BackendUrl(), response.Error) + reply := message.NewErrorServerMessage(NewError("add_failed", response.Error.Error())) session.SendMessage(reply) return } } else { - request := talk.NewBackendClientSessionRequest(room.Id(), "add", publicSessionId, msg) - var response talk.BackendClientSessionResponse - if err := h.backend.PerformJSONRequest(ctx, session.ParsedBackendOcsUrl(), request, &response); err != nil { + request := NewBackendClientSessionRequest(room.Id(), "add", publicSessionId, msg) + var response BackendClientSessionResponse + if err := h.backend.PerformJSONRequest(ctx, session.ParsedBackendUrl(), request, &response); err != nil { sess.Close() - h.logger.Printf("Could not add virtual session %s at backend %s: %s", virtualSessionId, session.BackendUrl(), err) - reply := message.NewErrorServerMessage(api.NewError("add_failed", "Could not add virtual session.")) + log.Printf("Could not add virtual session %s at backend %s: %s", virtualSessionId, session.BackendUrl(), err) + reply := message.NewErrorServerMessage(NewError("add_failed", "Could not add virtual session.")) session.SendMessage(reply) return } @@ -2649,17 +2359,17 @@ func (h *Hub) processInternalMsg(sess Session, message *api.ClientMessage) { h.sessions[sessionIdData.Sid] = sess h.virtualSessions[virtualSessionId] = sessionIdData.Sid h.mu.Unlock() - statsHubSessionsCurrent.WithLabelValues(session.Backend().Id(), string(sess.ClientType())).Inc() - statsHubSessionsTotal.WithLabelValues(session.Backend().Id(), string(sess.ClientType())).Inc() - h.logger.Printf("Session %s added virtual session %s with initial flags %d", session.PublicId(), sess.PublicId(), sess.Flags()) + statsHubSessionsCurrent.WithLabelValues(session.Backend().Id(), sess.ClientType()).Inc() + statsHubSessionsTotal.WithLabelValues(session.Backend().Id(), sess.ClientType()).Inc() + log.Printf("Session %s added virtual session %s with initial flags %d", session.PublicId(), sess.PublicId(), sess.Flags()) session.AddVirtualSession(sess) - sess.SetRoom(room, time.Now()) + sess.SetRoom(room) room.AddSession(sess, nil) case "updatesession": msg := msg.UpdateSession room := h.GetRoomForBackend(msg.RoomId, session.Backend()) if room == nil { - h.logger.Printf("Ignore remove session message %+v for invalid room %s from %s", *msg, msg.RoomId, session.PublicId()) + log.Printf("Ignore remove session message %+v for invalid room %s from %s", *msg, msg.RoomId, session.PublicId()) return } @@ -2687,7 +2397,7 @@ func (h *Hub) processInternalMsg(sess Session, message *api.ClientMessage) { } } } else { - h.logger.Printf("Ignore update request for non-virtual session %s", sess.PublicId()) + log.Printf("Ignore update request for non-virtual session %s", sess.PublicId()) } if changed != 0 { room.NotifySessionChanged(sess, changed) @@ -2697,7 +2407,7 @@ func (h *Hub) processInternalMsg(sess Session, message *api.ClientMessage) { msg := msg.RemoveSession room := h.GetRoomForBackend(msg.RoomId, session.Backend()) if room == nil { - h.logger.Printf("Ignore remove session message %+v for invalid room %s from %s", *msg, msg.RoomId, session.PublicId()) + log.Printf("Ignore remove session message %+v for invalid room %s from %s", *msg, msg.RoomId, session.PublicId()) return } @@ -2713,7 +2423,7 @@ func (h *Hub) processInternalMsg(sess Session, message *api.ClientMessage) { sess := h.sessions[sid] h.mu.Unlock() if sess != nil { - h.logger.Printf("Session %s removed virtual session %s", session.PublicId(), sess.PublicId()) + log.Printf("Session %s removed virtual session %s", session.PublicId(), sess.PublicId()) if vsess, ok := sess.(*VirtualSession); ok { // We should always have a VirtualSession here. vsess.CloseWithFeedback(session, message) @@ -2732,71 +2442,57 @@ func (h *Hub) processInternalMsg(sess Session, message *api.ClientMessage) { roomId := msg.Dialout.RoomId msg.Dialout.RoomId = "" // Don't send room id to recipients. if msg.Dialout.Type == "status" { - asyncMessage := &events.AsyncMessage{ + asyncMessage := &AsyncMessage{ Type: "room", - Room: &talk.BackendServerRoomRequest{ + Room: &BackendServerRoomRequest{ Type: "transient", - Transient: &talk.BackendRoomTransientRequest{ - Action: talk.TransientActionSet, + Transient: &BackendRoomTransientRequest{ + Action: TransientActionSet, Key: "callstatus_" + msg.Dialout.Status.CallId, Value: msg.Dialout.Status, }, }, } - if msg.Dialout.Status.Status == api.DialoutStatusCleared || msg.Dialout.Status.Status == api.DialoutStatusRejected { + if msg.Dialout.Status.Status == DialoutStatusCleared || msg.Dialout.Status.Status == DialoutStatusRejected { asyncMessage.Room.Transient.TTL = removeCallStatusTTL } if err := h.events.PublishBackendRoomMessage(roomId, session.Backend(), asyncMessage); err != nil { - h.logger.Printf("Error publishing dialout message %+v to room %s", msg.Dialout, roomId) + log.Printf("Error publishing dialout message %+v to room %s", msg.Dialout, roomId) } } else { - if err := h.events.PublishRoomMessage(roomId, session.Backend(), &events.AsyncMessage{ + if err := h.events.PublishRoomMessage(roomId, session.Backend(), &AsyncMessage{ Type: "message", - Message: &api.ServerMessage{ + Message: &ServerMessage{ Type: "dialout", Dialout: msg.Dialout, }, }); err != nil { - h.logger.Printf("Error publishing dialout message %+v to room %s", msg.Dialout, roomId) + log.Printf("Error publishing dialout message %+v to room %s", msg.Dialout, roomId) } } default: - h.logger.Printf("Ignore unsupported internal message %+v from %s", msg, session.PublicId()) + log.Printf("Ignore unsupported internal message %+v from %s", msg, session.PublicId()) return } } func isAllowedToUpdateTransientData(session Session) bool { - if session.ClientType() == api.HelloClientTypeInternal { + if session.ClientType() == HelloClientTypeInternal { // Internal clients are always allowed. return true } - if session.HasPermission(api.PERMISSION_TRANSIENT_DATA) { + if session.HasPermission(PERMISSION_TRANSIENT_DATA) { return true } return false } -func isAllowedToUpdateTransientDataKey(session Session, key string) bool { - if session.ClientType() == api.HelloClientTypeInternal { - // Internal clients may update all transient keys. - return true - } - - if sid, found := strings.CutPrefix(key, api.TransientSessionDataPrefix); found { - // Session data may only be modified by the session itself. - return sid == string(session.PublicId()) - } - - return true -} - -func (h *Hub) processTransientMsg(session Session, message *api.ClientMessage) { +func (h *Hub) processTransientMsg(session Session, message *ClientMessage) { room := session.GetRoom() if room == nil { - response := message.NewErrorServerMessage(api.NewError("not_in_room", "No room joined yet.")) + response := message.NewErrorServerMessage(NewError("not_in_room", "No room joined yet.")) session.SendMessage(response) return } @@ -2807,52 +2503,42 @@ func (h *Hub) processTransientMsg(session Session, message *api.ClientMessage) { if !isAllowedToUpdateTransientData(session) { sendNotAllowed(session, message, "Not allowed to update transient data.") return - } else if !isAllowedToUpdateTransientDataKey(session, msg.Key) { - sendNotAllowed(session, message, "Not allowed to update this transient data entry.") - return } - if err := room.SetTransientDataTTL(msg.Key, msg.Value, msg.TTL); err != nil { - response := message.NewWrappedErrorServerMessage(err) - session.SendMessage(response) - return + if msg.Value == nil { + room.SetTransientDataTTL(msg.Key, nil, msg.TTL) + } else { + room.SetTransientDataTTL(msg.Key, msg.Value, msg.TTL) } case "remove": if !isAllowedToUpdateTransientData(session) { sendNotAllowed(session, message, "Not allowed to update transient data.") return - } else if !isAllowedToUpdateTransientDataKey(session, msg.Key) { - sendNotAllowed(session, message, "Not allowed to update this transient data entry.") - return } - if err := room.RemoveTransientData(msg.Key); err != nil { - response := message.NewWrappedErrorServerMessage(err) - session.SendMessage(response) - return - } + room.RemoveTransientData(msg.Key) default: - response := message.NewErrorServerMessage(api.NewError("ignored", "Unsupported message type.")) + response := message.NewErrorServerMessage(NewError("ignored", "Unsupported message type.")) session.SendMessage(response) } } -func sendNotAllowed(session Session, message *api.ClientMessage, reason string) { - response := message.NewErrorServerMessage(api.NewError("not_allowed", reason)) +func sendNotAllowed(session Session, message *ClientMessage, reason string) { + response := message.NewErrorServerMessage(NewError("not_allowed", reason)) session.SendMessage(response) } -func sendMcuClientNotFound(session Session, message *api.ClientMessage) { - response := message.NewErrorServerMessage(api.NewError("client_not_found", "No MCU client found to send message to.")) +func sendMcuClientNotFound(session Session, message *ClientMessage) { + response := message.NewErrorServerMessage(NewError("client_not_found", "No MCU client found to send message to.")) session.SendMessage(response) } -func sendMcuProcessingFailed(session Session, message *api.ClientMessage) { - response := message.NewErrorServerMessage(api.NewError("processing_failed", "Processing of the message failed, please check server logs.")) +func sendMcuProcessingFailed(session Session, message *ClientMessage) { + response := message.NewErrorServerMessage(NewError("processing_failed", "Processing of the message failed, please check server logs.")) session.SendMessage(response) } -func (h *Hub) isInSameCallRemote(ctx context.Context, senderSession *ClientSession, senderRoom *Room, recipientSessionId api.PublicSessionId) bool { +func (h *Hub) isInSameCallRemote(ctx context.Context, senderSession *ClientSession, senderRoom *Room, recipientSessionId string) bool { clients := h.rpcClients.GetClients() if len(clients) == 0 { return false @@ -2863,12 +2549,15 @@ func (h *Hub) isInSameCallRemote(ctx context.Context, senderSession *ClientSessi rpcCtx, cancel := context.WithCancel(ctx) defer cancel() for _, client := range clients { - wg.Go(func() { - inCall, err := client.IsSessionInCall(rpcCtx, recipientSessionId, senderRoom.Id(), senderSession.BackendUrl()) + wg.Add(1) + go func(client *GrpcClient) { + defer wg.Done() + + inCall, err := client.IsSessionInCall(rpcCtx, recipientSessionId, senderRoom) if errors.Is(err, context.Canceled) { return } else if err != nil { - h.logger.Printf("Error checking session %s in call on %s: %s", recipientSessionId, client.Target(), err) + log.Printf("Error checking session %s in call on %s: %s", recipientSessionId, client.Target(), err) return } else if !inCall { return @@ -2876,15 +2565,15 @@ func (h *Hub) isInSameCallRemote(ctx context.Context, senderSession *ClientSessi cancel() result.Store(true) - }) + }(client) } wg.Wait() return result.Load() } -func (h *Hub) isInSameCall(ctx context.Context, senderSession *ClientSession, recipientSessionId api.PublicSessionId) bool { - if senderSession.ClientType() == api.HelloClientTypeInternal { +func (h *Hub) isInSameCall(ctx context.Context, senderSession *ClientSession, recipientSessionId string) bool { + if senderSession.ClientType() == HelloClientTypeInternal { // Internal clients may subscribe all streams. return true } @@ -2903,7 +2592,7 @@ func (h *Hub) isInSameCall(ctx context.Context, senderSession *ClientSession, re recipientRoom := recipientSession.GetRoom() if recipientRoom == nil || !senderRoom.IsEqual(recipientRoom) || - (recipientSession.ClientType() != api.HelloClientTypeInternal && !recipientRoom.IsSessionInCall(recipientSession)) { + (recipientSession.ClientType() != HelloClientTypeInternal && !recipientRoom.IsSessionInCall(recipientSession)) { // Recipient is not in a room, a different room or not in the call. return false } @@ -2911,87 +2600,80 @@ func (h *Hub) isInSameCall(ctx context.Context, senderSession *ClientSession, re return true } -func (h *Hub) processMcuMessage(session *ClientSession, client_message *api.ClientMessage, message *api.MessageClientMessage, data *api.MessageClientMessageData) { +func (h *Hub) processMcuMessage(session *ClientSession, client_message *ClientMessage, message *MessageClientMessage, data *MessageClientMessageData) { ctx, cancel := context.WithTimeout(session.Context(), h.mcuTimeout) defer cancel() - var mc sfu.Client + var mc McuClient var err error var clientType string switch data.Type { case "requestoffer": if session.PublicId() == message.Recipient.SessionId { - h.logger.Printf("Not requesting offer from itself for session %s", session.PublicId()) + log.Printf("Not requesting offer from itself for session %s", session.PublicId()) return } // A user is only allowed to subscribe a stream if she is in the same room // as the other user and both have their "inCall" flag set. if !h.allowSubscribeAnyStream && !h.isInSameCall(ctx, session, message.Recipient.SessionId) { - h.logger.Printf("Session %s is not in the same call as session %s, not requesting offer", session.PublicId(), message.Recipient.SessionId) + log.Printf("Session %s is not in the same call as session %s, not requesting offer", session.PublicId(), message.Recipient.SessionId) sendNotAllowed(session, client_message, "Not allowed to request offer.") return } clientType = "subscriber" - mc, err = session.GetOrCreateSubscriber(ctx, h.mcu, message.Recipient.SessionId, sfu.StreamType(data.RoomType)) + mc, err = session.GetOrCreateSubscriber(ctx, h.mcu, message.Recipient.SessionId, StreamType(data.RoomType)) case "sendoffer": // Will be sent directly. return case "offer": clientType = "publisher" - mc, err = session.GetOrCreatePublisher(ctx, h.mcu, sfu.StreamType(data.RoomType), data) + mc, err = session.GetOrCreatePublisher(ctx, h.mcu, StreamType(data.RoomType), data) if err, ok := err.(*PermissionError); ok { - h.logger.Printf("Session %s is not allowed to offer %s, ignoring (%s)", session.PublicId(), data.RoomType, err) + log.Printf("Session %s is not allowed to offer %s, ignoring (%s)", session.PublicId(), data.RoomType, err) sendNotAllowed(session, client_message, "Not allowed to publish.") return } case "selectStream": if session.PublicId() == message.Recipient.SessionId { - h.logger.Printf("Not selecting substream for own %s stream in session %s", data.RoomType, session.PublicId()) + log.Printf("Not selecting substream for own %s stream in session %s", data.RoomType, session.PublicId()) return } clientType = "subscriber" - mc = session.GetSubscriber(message.Recipient.SessionId, sfu.StreamType(data.RoomType)) + mc = session.GetSubscriber(message.Recipient.SessionId, StreamType(data.RoomType)) default: - if data.Type == "candidate" && api.FilterCandidate(data.Candidate, h.allowedCandidates.Load(), h.blockedCandidates.Load()) { - // Silently ignore filtered candidates. - return - } - if session.PublicId() == message.Recipient.SessionId { if err := session.IsAllowedToSend(data); err != nil { - h.logger.Printf("Session %s is not allowed to send candidate for %s, ignoring (%s)", session.PublicId(), data.RoomType, err) + log.Printf("Session %s is not allowed to send candidate for %s, ignoring (%s)", session.PublicId(), data.RoomType, err) sendNotAllowed(session, client_message, "Not allowed to send candidate.") return } clientType = "publisher" - mc = session.GetPublisher(sfu.StreamType(data.RoomType)) + mc = session.GetPublisher(StreamType(data.RoomType)) } else { clientType = "subscriber" - mc = session.GetSubscriber(message.Recipient.SessionId, sfu.StreamType(data.RoomType)) + mc = session.GetSubscriber(message.Recipient.SessionId, StreamType(data.RoomType)) } } if err != nil { - h.logger.Printf("Could not create MCU %s for session %s to send %+v to %s: %s", clientType, session.PublicId(), data, message.Recipient.SessionId, err) + log.Printf("Could not create MCU %s for session %s to send %+v to %s: %s", clientType, session.PublicId(), data, message.Recipient.SessionId, err) sendMcuClientNotFound(session, client_message) return } else if mc == nil { - h.logger.Printf("No MCU %s found for session %s to send %+v to %s", clientType, session.PublicId(), data, message.Recipient.SessionId) + log.Printf("No MCU %s found for session %s to send %+v to %s", clientType, session.PublicId(), data, message.Recipient.SessionId) sendMcuClientNotFound(session, client_message) return } - mc.SendMessage(session.Context(), message, data, func(err error, response api.StringMap) { + mc.SendMessage(session.Context(), message, data, func(err error, response map[string]interface{}) { if err != nil { - if !errors.Is(err, api.ErrCandidateFiltered) { - h.logger.Printf("Could not send MCU message %+v for session %s to %s: %s", data, session.PublicId(), message.Recipient.SessionId, err) - sendMcuProcessingFailed(session, client_message) - } + log.Printf("Could not send MCU message %+v for session %s to %s: %s", data, session.PublicId(), message.Recipient.SessionId, err) + sendMcuProcessingFailed(session, client_message) return - } else if len(response) == 0 { + } else if response == nil { // No response received return } @@ -3000,11 +2682,11 @@ func (h *Hub) processMcuMessage(session *ClientSession, client_message *api.Clie }) } -func (h *Hub) sendMcuMessageResponse(session *ClientSession, mcuClient sfu.Client, message *api.MessageClientMessage, data *api.MessageClientMessageData, response api.StringMap) { - var response_message *api.ServerMessage +func (h *Hub) sendMcuMessageResponse(session *ClientSession, mcuClient McuClient, message *MessageClientMessage, data *MessageClientMessageData, response map[string]interface{}) { + var response_message *ServerMessage switch response["type"] { case "answer": - answer_message := &api.AnswerOfferMessage{ + answer_message := &AnswerOfferMessage{ To: session.PublicId(), From: session.PublicId(), Type: "answer", @@ -3014,13 +2696,13 @@ func (h *Hub) sendMcuMessageResponse(session *ClientSession, mcuClient sfu.Clien } answer_data, err := json.Marshal(answer_message) if err != nil { - h.logger.Printf("Could not serialize answer %+v to %s: %s", answer_message, session.PublicId(), err) + log.Printf("Could not serialize answer %+v to %s: %s", answer_message, session.PublicId(), err) return } - response_message = &api.ServerMessage{ + response_message = &ServerMessage{ Type: "message", - Message: &api.MessageServerMessage{ - Sender: &api.MessageServerMessageSender{ + Message: &MessageServerMessage{ + Sender: &MessageServerMessageSender{ Type: "session", SessionId: session.PublicId(), UserId: session.UserId(), @@ -3029,7 +2711,7 @@ func (h *Hub) sendMcuMessageResponse(session *ClientSession, mcuClient sfu.Clien }, } case "offer": - offer_message := &api.AnswerOfferMessage{ + offer_message := &AnswerOfferMessage{ To: session.PublicId(), From: message.Recipient.SessionId, Type: "offer", @@ -3039,13 +2721,13 @@ func (h *Hub) sendMcuMessageResponse(session *ClientSession, mcuClient sfu.Clien } offer_data, err := json.Marshal(offer_message) if err != nil { - h.logger.Printf("Could not serialize offer %+v to %s: %s", offer_message, session.PublicId(), err) + log.Printf("Could not serialize offer %+v to %s: %s", offer_message, session.PublicId(), err) return } - response_message = &api.ServerMessage{ + response_message = &ServerMessage{ Type: "message", - Message: &api.MessageServerMessage{ - Sender: &api.MessageServerMessageSender{ + Message: &MessageServerMessage{ + Sender: &MessageServerMessageSender{ Type: "session", SessionId: message.Recipient.SessionId, // TODO(jojo): Set "UserId" field if known user. @@ -3054,35 +2736,27 @@ func (h *Hub) sendMcuMessageResponse(session *ClientSession, mcuClient sfu.Clien }, } default: - h.logger.Printf("Unsupported response %+v received to send to %s", response, session.PublicId()) + log.Printf("Unsupported response %+v received to send to %s", response, session.PublicId()) return } session.SendMessage(response_message) } -func (h *Hub) processByeMsg(client ClientWithSession, message *api.ClientMessage) { +func (h *Hub) processByeMsg(client HandlerClient, message *ClientMessage) { client.SendByeResponse(message) if session := h.processUnregister(client); session != nil { session.Close() } } -func (h *Hub) processRoomUpdated(message *talk.BackendServerRoomRequest) { - room := h.GetRoomForBackend(message.RoomId, message.Backend) - if room == nil { - return - } - +func (h *Hub) processRoomUpdated(message *BackendServerRoomRequest) { + room := message.room room.UpdateProperties(message.Update.Properties) } -func (h *Hub) processRoomDeleted(message *talk.BackendServerRoomRequest) { - room := h.GetRoomForBackend(message.RoomId, message.Backend) - if room == nil { - return - } - +func (h *Hub) processRoomDeleted(message *BackendServerRoomRequest) { + room := message.room sessions := room.Close() for _, session := range sessions { // The session is no longer in the room @@ -3096,18 +2770,14 @@ func (h *Hub) processRoomDeleted(message *talk.BackendServerRoomRequest) { } } -func (h *Hub) processRoomInCallChanged(message *talk.BackendServerRoomRequest) { - room := h.GetRoomForBackend(message.RoomId, message.Backend) - if room == nil { - return - } - +func (h *Hub) processRoomInCallChanged(message *BackendServerRoomRequest) { + room := message.room if message.InCall.All { var flags int if err := json.Unmarshal(message.InCall.InCall, &flags); err != nil { var incall bool if err := json.Unmarshal(message.InCall.InCall, &incall); err != nil { - h.logger.Printf("Unsupported InCall flags type: %+v, ignoring", string(message.InCall.InCall)) + log.Printf("Unsupported InCall flags type: %+v, ignoring", string(message.InCall.InCall)) return } @@ -3122,17 +2792,13 @@ func (h *Hub) processRoomInCallChanged(message *talk.BackendServerRoomRequest) { } } -func (h *Hub) processRoomParticipants(message *talk.BackendServerRoomRequest) { - room := h.GetRoomForBackend(message.RoomId, message.Backend) - if room == nil { - return - } - +func (h *Hub) processRoomParticipants(message *BackendServerRoomRequest) { + room := message.room room.PublishUsersChanged(message.Participants.Changed, message.Participants.Users) } -func (h *Hub) GetStats() api.StringMap { - result := make(api.StringMap) +func (h *Hub) GetStats() map[string]interface{} { + result := make(map[string]interface{}) h.ru.RLock() result["rooms"] = len(h.rooms) h.ru.RUnlock() @@ -3147,41 +2813,66 @@ func (h *Hub) GetStats() api.StringMap { return result } -func (h *Hub) GetServerInfoDialout() (result []talk.BackendServerInfoDialout) { - h.mu.RLock() - defer h.mu.RUnlock() - - for session := range h.dialoutSessions { - dialout := talk.BackendServerInfoDialout{ - SessionId: session.PublicId(), - } - if client := session.GetClient(); client != nil && client.IsConnected() { - dialout.Connected = true - dialout.Address = client.RemoteAddr() - if ua := client.UserAgent(); ua != "" { - dialout.UserAgent = ua - // Extract version from user-agent, expects "software/version". - if pos := strings.IndexByte(ua, '/'); pos != -1 { - version := ua[pos+1:] - if pos = strings.IndexByte(version, ' '); pos != -1 { - version = version[:pos] - } - dialout.Version = version - } - } - dialout.Features = session.GetFeatures() - } - result = append(result, dialout) +func GetRealUserIP(r *http.Request, trusted *AllowedIps) string { + addr := r.RemoteAddr + if host, _, err := net.SplitHostPort(addr); err == nil { + addr = host } - slices.SortFunc(result, func(a, b talk.BackendServerInfoDialout) int { - return strings.Compare(string(a.SessionId), string(b.SessionId)) - }) - return + ip := net.ParseIP(addr) + if len(ip) == 0 { + return addr + } + + // Don't check any headers if the server can be reached by untrusted clients directly. + if trusted == nil || !trusted.Allowed(ip) { + return addr + } + + if realIP := r.Header.Get("X-Real-IP"); realIP != "" { + if ip := net.ParseIP(realIP); len(ip) > 0 { + return realIP + } + } + + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address + forwarded := strings.Split(strings.Join(r.Header.Values("X-Forwarded-For"), ","), ",") + if len(forwarded) > 0 { + slices.Reverse(forwarded) + var lastTrusted string + for _, hop := range forwarded { + hop = strings.TrimSpace(hop) + // Make sure to remove any port. + if host, _, err := net.SplitHostPort(hop); err == nil { + hop = host + } + + ip := net.ParseIP(hop) + if len(ip) == 0 { + continue + } + + if trusted.Allowed(ip) { + lastTrusted = hop + continue + } + + return hop + } + + // If all entries in the "X-Forwarded-For" list are trusted, the left-most + // will be the client IP. This can happen if a subnet is trusted and the + // client also has an IP from this subnet. + if lastTrusted != "" { + return lastTrusted + } + } + + return addr } func (h *Hub) getRealUserIP(r *http.Request) string { - return client.GetRealUserIP(r, h.trustedProxies.Load()) + return GetRealUserIP(r, h.trustedProxies.Load()) } func (h *Hub) serveWs(w http.ResponseWriter, r *http.Request) { @@ -3194,19 +2885,13 @@ func (h *Hub) serveWs(w http.ResponseWriter, r *http.Request) { conn, err := h.upgrader.Upgrade(w, r, header) if err != nil { - h.logger.Printf("Could not upgrade request from %s: %s", addr, err) + log.Printf("Could not upgrade request from %s: %s", addr, err) return } - ctx := log.NewLoggerContext(r.Context(), h.logger) - if conn.Subprotocol() == janus.EventsSubprotocol { - janus.RunEventsHandler(ctx, h.mcu, conn, addr, agent) - return - } - - client, err := NewHubClient(ctx, conn, addr, agent, h) + client, err := NewClient(r.Context(), conn, addr, agent, h) if err != nil { - h.logger.Printf("Could not create client for %s: %s", addr, err) + log.Printf("Could not create client for %s: %s", addr, err) return } @@ -3222,48 +2907,52 @@ func (h *Hub) serveWs(w http.ResponseWriter, r *http.Request) { client.ReadPump() } -func (h *Hub) ProxySession(request grpc.RpcSessions_ProxySessionServer) error { - client, err := newRemoteGrpcClient(h, request) - if err != nil { - return err - } - - sid := h.registerClient(client) - defer h.unregisterClient(sid) - - return client.run() -} - -func (h *Hub) LookupCountry(addr string) geoip.Country { - ip := net.ParseIP(addr) +func (h *Hub) OnLookupCountry(client HandlerClient) string { + ip := net.ParseIP(client.RemoteAddr()) if ip == nil { - return geoip.NoCountry + return noCountry } - if country, found := h.geoipOverrides.Load().Lookup(ip); found { - return country + if overrides := h.geoipOverrides.Load(); overrides != nil { + for overrideNet, country := range *overrides { + if overrideNet.Contains(ip) { + return country + } + } } if ip.IsLoopback() { - return geoip.Loopback + return loopback } - country := geoip.UnknownCountry + country := unknownCountry if h.geoip != nil { var err error country, err = h.geoip.LookupCountry(ip) if err != nil { - h.logger.Printf("Could not lookup country for %s: %s", ip, err) - return geoip.UnknownCountry + log.Printf("Could not lookup country for %s: %s", ip, err) + return unknownCountry } if country == "" { - country = geoip.UnknownCountry + country = unknownCountry } } return country } +func (h *Hub) OnClosed(client HandlerClient) { + h.processUnregister(client) +} + +func (h *Hub) OnMessageReceived(client HandlerClient, data []byte) { + h.processMessage(client, data) +} + +func (h *Hub) OnRTTReceived(client HandlerClient, rtt time.Duration) { + // Ignore +} + func (h *Hub) ShutdownChannel() <-chan struct{} { return h.shutdown.C } diff --git a/server/hub_stats_prometheus.go b/hub_stats_prometheus.go similarity index 92% rename from server/hub_stats_prometheus.go rename to hub_stats_prometheus.go index 9082dd9..f3d5c1a 100644 --- a/server/hub_stats_prometheus.go +++ b/hub_stats_prometheus.go @@ -19,16 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" ) var ( - statsHubRoomsCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ // +checklocksignore: Global readonly variable. + statsHubRoomsCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "signaling", Subsystem: "hub", Name: "rooms", @@ -63,11 +61,10 @@ var ( statsHubRoomsCurrent, statsHubSessionsCurrent, statsHubSessionsTotal, - statsHubSessionsResumedTotal, statsHubSessionResumeFailed, } ) func RegisterHubStats() { - metrics.RegisterAll(hubStats...) + registerAll(hubStats...) } diff --git a/server/hub_test.go b/hub_test.go similarity index 58% rename from server/hub_test.go rename to hub_test.go index ed57b89..0d4bf48 100644 --- a/server/hub_test.go +++ b/hub_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" @@ -33,7 +33,6 @@ import ( "encoding/json" "encoding/pem" "errors" - "fmt" "io" "net/http" "net/http/httptest" @@ -48,27 +47,8 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/gorilla/mux" "github.com/gorilla/websocket" - "github.com/nats-io/nats-server/v2/server" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - eventstest "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - grpctest "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" - natstest "github.com/strukturag/nextcloud-spreed-signaling/v2/nats/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/session" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - sfutest "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" ) const ( @@ -150,24 +130,7 @@ func getTestConfigWithMultipleBackends(server *httptest.Server) (*goconf.ConfigF return config, nil } -func getTestConfigWithMultipleUrls(server *httptest.Server) (*goconf.ConfigFile, error) { - config, err := getTestConfig(server) - if err != nil { - return nil, err - } - - config.RemoveOption("backend", "allowed") - config.RemoveOption("backend", "secret") - config.AddOption("backend", "backends", "backend1") - - config.AddOption("backend1", "urls", strings.Join([]string{server.URL + "/one", server.URL + "/two/"}, ",")) - config.AddOption("backend1", "secret", string(testBackendSecret)) - return config, nil -} - -func CreateHubForTestWithConfig(t *testing.T, getConfigFunc func(*httptest.Server) (*goconf.ConfigFile, error)) (*Hub, events.AsyncEvents, *mux.Router, *httptest.Server) { - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) +func CreateHubForTestWithConfig(t *testing.T, getConfigFunc func(*httptest.Server) (*goconf.ConfigFile, error)) (*Hub, AsyncEvents, *mux.Router, *httptest.Server) { require := require.New(t) r := mux.NewRouter() registerBackendHandler(t, r) @@ -177,12 +140,12 @@ func CreateHubForTestWithConfig(t *testing.T, getConfigFunc func(*httptest.Serve server.Close() }) - events := eventstest.GetAsyncEventsForTest(t) + events := getAsyncEventsForTest(t) config, err := getConfigFunc(server) require.NoError(err) - h, err := NewHub(ctx, config, events, nil, nil, nil, r, "no-version") + h, err := NewHub(config, events, nil, nil, nil, r, "no-version") require.NoError(err) - b, err := NewBackendServer(ctx, config, h, "no-version") + b, err := NewBackendServer(config, h, "no-version") require.NoError(err) require.NoError(b.Start(r)) @@ -198,29 +161,19 @@ func CreateHubForTestWithConfig(t *testing.T, getConfigFunc func(*httptest.Serve return h, events, r, server } -func CreateHubForTest(t *testing.T) (*Hub, events.AsyncEvents, *mux.Router, *httptest.Server) { +func CreateHubForTest(t *testing.T) (*Hub, AsyncEvents, *mux.Router, *httptest.Server) { return CreateHubForTestWithConfig(t, getTestConfig) } -func CreateHubWithMultipleBackendsForTest(t *testing.T) (*Hub, events.AsyncEvents, *mux.Router, *httptest.Server) { +func CreateHubWithMultipleBackendsForTest(t *testing.T) (*Hub, AsyncEvents, *mux.Router, *httptest.Server) { h, events, r, server := CreateHubForTestWithConfig(t, getTestConfigWithMultipleBackends) registerBackendHandlerUrl(t, r, "/one") registerBackendHandlerUrl(t, r, "/two") return h, events, r, server } -func CreateHubWithMultipleUrlsForTest(t *testing.T) (*Hub, events.AsyncEvents, *mux.Router, *httptest.Server) { - h, events, r, server := CreateHubForTestWithConfig(t, getTestConfigWithMultipleUrls) - registerBackendHandlerUrl(t, r, "/one") - registerBackendHandlerUrl(t, r, "/two") - return h, events, r, server -} - func CreateClusteredHubsForTestWithConfig(t *testing.T, getConfigFunc func(*httptest.Server) (*goconf.ConfigFile, error)) (*Hub, *Hub, *mux.Router, *mux.Router, *httptest.Server, *httptest.Server) { - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) require := require.New(t) - assert := assert.New(t) r1 := mux.NewRouter() registerBackendHandler(t, r1) @@ -237,48 +190,44 @@ func CreateClusteredHubsForTestWithConfig(t *testing.T, getConfigFunc func(*http server2.Close() }) - nats1, _ := natstest.StartLocalServer(t) - var nats2 *server.Server + nats1 := startLocalNatsServer(t) + var nats2 string if strings.Contains(t.Name(), "Federation") { - nats2, _ = natstest.StartLocalServer(t) + nats2 = startLocalNatsServer(t) } else { nats2 = nats1 } - grpcServer1, addr1 := grpctest.NewServerForTest(t) - grpcServer2, addr2 := grpctest.NewServerForTest(t) + grpcServer1, addr1 := NewGrpcServerForTest(t) + grpcServer2, addr2 := NewGrpcServerForTest(t) if strings.Contains(t.Name(), "Federation") { // Signaling servers should not form a cluster in federation tests. addr1, addr2 = addr2, addr1 } - events1, err := events.NewAsyncEvents(ctx, nats1.ClientURL()) + events1, err := NewAsyncEvents(nats1) require.NoError(err) t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - assert.NoError(events1.Close(ctx)) + events1.Close() }) config1, err := getConfigFunc(server1) require.NoError(err) - client1, _ := grpctest.NewClientsForTest(t, addr2, nil) - h1, err := NewHub(ctx, config1, events1, grpcServer1, client1, nil, r1, "no-version") + client1, _ := NewGrpcClientsForTest(t, addr2) + h1, err := NewHub(config1, events1, grpcServer1, client1, nil, r1, "no-version") require.NoError(err) - b1, err := NewBackendServer(ctx, config1, h1, "no-version") + b1, err := NewBackendServer(config1, h1, "no-version") require.NoError(err) - events2, err := events.NewAsyncEvents(ctx, nats2.ClientURL()) + events2, err := NewAsyncEvents(nats2) require.NoError(err) t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - assert.NoError(events2.Close(ctx)) + events2.Close() }) config2, err := getConfigFunc(server2) require.NoError(err) - client2, _ := grpctest.NewClientsForTest(t, addr1, nil) - h2, err := NewHub(ctx, config2, events2, grpcServer2, client2, nil, r2, "no-version") + client2, _ := NewGrpcClientsForTest(t, addr1) + h2, err := NewHub(config2, events2, grpcServer2, client2, nil, r2, "no-version") require.NoError(err) - b2, err := NewBackendServer(ctx, config2, h2, "no-version") + b2, err := NewBackendServer(config2, h2, "no-version") require.NoError(err) require.NoError(b1.Start(r1)) require.NoError(b2.Start(r2)) @@ -311,8 +260,6 @@ func WaitForHub(ctx context.Context, t *testing.T, h *Hub) { clients := len(h.clients) sessions := len(h.sessions) remoteSessions := len(h.remoteSessions) - federatedSessions := len(h.federatedSessions) - federationClients := len(h.federatedSessions) h.mu.Unlock() h.ru.Lock() rooms := len(h.rooms) @@ -323,8 +270,6 @@ func WaitForHub(ctx context.Context, t *testing.T, h *Hub) { rooms == 0 && sessions == 0 && remoteSessions == 0 && - federatedSessions == 0 && - federationClients == 0 && readActive == 0 && writeActive == 0 { break @@ -334,18 +279,8 @@ func WaitForHub(ctx context.Context, t *testing.T, h *Hub) { case <-ctx.Done(): h.mu.Lock() h.ru.Lock() - test.DumpGoroutines("", os.Stderr) - assert.Fail(t, "Error waiting for hub to terminate", "clients %+v / rooms %+v / sessions %+v / remoteSessions %v / federatedSessions %v / federationClients %v / %d read / %d write: %s", - h.clients, - h.rooms, - h.sessions, - remoteSessions, - federatedSessions, - federationClients, - readActive, - writeActive, - ctx.Err(), - ) + dumpGoroutines("", os.Stderr) + assert.Fail(t, "Error waiting for clients %+v / rooms %+v / sessions %+v / remoteSessions %v / %d read / %d write to terminate: %s", h.clients, h.rooms, h.sessions, h.remoteSessions, readActive, writeActive, ctx.Err()) h.ru.Unlock() h.mu.Unlock() return @@ -355,24 +290,24 @@ func WaitForHub(ctx context.Context, t *testing.T, h *Hub) { } } -func validateBackendChecksum(t *testing.T, f func(http.ResponseWriter, *http.Request, *talk.BackendClientRequest) *talk.BackendClientResponse) func(http.ResponseWriter, *http.Request) { +func validateBackendChecksum(t *testing.T, f func(http.ResponseWriter, *http.Request, *BackendClientRequest) *BackendClientResponse) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - assert := assert.New(t) + require := require.New(t) body, err := io.ReadAll(r.Body) - assert.NoError(err) + require.NoError(err) - rnd := r.Header.Get(talk.HeaderBackendSignalingRandom) - checksum := r.Header.Get(talk.HeaderBackendSignalingChecksum) + rnd := r.Header.Get(HeaderBackendSignalingRandom) + checksum := r.Header.Get(HeaderBackendSignalingChecksum) if rnd == "" || checksum == "" { - assert.Fail("No checksum headers found", "request to %s", r.URL) + require.Fail("No checksum headers found in request to %s", r.URL) } - if verify := talk.CalculateBackendChecksum(rnd, body, testBackendSecret); verify != checksum { - assert.Fail("Backend checksum verification failed", "request to %s", r.URL) + if verify := CalculateBackendChecksum(rnd, body, testBackendSecret); verify != checksum { + require.Fail("Backend checksum verification failed for request to %s", r.URL) } - var request talk.BackendClientRequest - assert.NoError(json.Unmarshal(body, &request)) + var request BackendClientRequest + require.NoError(json.Unmarshal(body, &request)) response := f(w, r, &request) if response == nil { @@ -381,12 +316,12 @@ func validateBackendChecksum(t *testing.T, f func(http.ResponseWriter, *http.Req } data, err := json.Marshal(response) - assert.NoError(err) + require.NoError(err) if r.Header.Get("OCS-APIRequest") != "" { - var ocs talk.OcsResponse - ocs.Ocs = &talk.OcsBody{ - Meta: talk.OcsMeta{ + var ocs OcsResponse + ocs.Ocs = &OcsBody{ + Meta: OcsMeta{ Status: "ok", StatusCode: http.StatusOK, Message: http.StatusText(http.StatusOK), @@ -394,7 +329,7 @@ func validateBackendChecksum(t *testing.T, f func(http.ResponseWriter, *http.Req Data: data, } data, err = json.Marshal(ocs) - assert.NoError(err) + require.NoError(err) } w.Header().Set("Content-Type", "application/json") @@ -403,43 +338,43 @@ func validateBackendChecksum(t *testing.T, f func(http.ResponseWriter, *http.Req } } -func processAuthRequest(t *testing.T, w http.ResponseWriter, r *http.Request, request *talk.BackendClientRequest) *talk.BackendClientResponse { +func processAuthRequest(t *testing.T, w http.ResponseWriter, r *http.Request, request *BackendClientRequest) *BackendClientResponse { require := require.New(t) if request.Type != "auth" || request.Auth == nil { - require.Fail("Expected an auth backend request", "received %+v", request) + require.Fail("Expected an auth backend request, got %+v", request) } var params TestBackendClientAuthParams if len(request.Auth.Params) > 0 { require.NoError(json.Unmarshal(request.Auth.Params, ¶ms)) } - switch params.UserId { - case "": + if params.UserId == "" { params.UserId = testDefaultUserId - case authAnonymousUserId: + } else if params.UserId == authAnonymousUserId { params.UserId = "" } - response := &talk.BackendClientResponse{ + response := &BackendClientResponse{ Type: "auth", - Auth: &talk.BackendClientAuthResponse{ - Version: talk.BackendVersion, + Auth: &BackendClientAuthResponse{ + Version: BackendVersion, UserId: params.UserId, }, } userdata := map[string]string{ "displayname": "Displayname " + params.UserId, } - data, _ := json.Marshal(userdata) + data, err := json.Marshal(userdata) + require.NoError(err) response.Auth.User = data return response } -func processRoomRequest(t *testing.T, w http.ResponseWriter, r *http.Request, request *talk.BackendClientRequest) *talk.BackendClientResponse { +func processRoomRequest(t *testing.T, w http.ResponseWriter, r *http.Request, request *BackendClientRequest) *BackendClientResponse { require := require.New(t) assert := assert.New(t) if request.Type != "room" || request.Room == nil { - require.Fail("Expected an room backend request", "received %+v", request) + require.Fail("Expected an room backend request, got %+v", request) } switch request.Room.RoomId { @@ -448,12 +383,12 @@ func processRoomRequest(t *testing.T, w http.ResponseWriter, r *http.Request, re case "test-room-takeover-room-session": // Additional checks for testcase "TestClientTakeoverRoomSession" if request.Room.Action == "leave" && request.Room.UserId == "test-userid1" { - assert.Fail("Should not receive \"leave\" event for first user", "received %+v", request.Room) + assert.Fail("Should not receive \"leave\" event for first user, received %+v", request.Room) } case "test-invalid-room": - response := &talk.BackendClientResponse{ + response := &BackendClientResponse{ Type: "error", - Error: &api.Error{ + Error: &Error{ Code: "no_such_room", Message: "The user is not invited to this room.", }, @@ -463,25 +398,20 @@ func processRoomRequest(t *testing.T, w http.ResponseWriter, r *http.Request, re if strings.Contains(t.Name(), "Federation") { // Check additional fields present for federated sessions. - if strings.Contains(string(request.Room.SessionId), "@federated") { - assert.Equal(api.ActorTypeFederatedUsers, request.Room.ActorType) + if strings.Contains(request.Room.SessionId, "@federated") { + assert.Equal(ActorTypeFederatedUsers, request.Room.ActorType) assert.NotEmpty(request.Room.ActorId) } else { assert.Empty(request.Room.ActorType) assert.Empty(request.Room.ActorId) } - } else if strings.Contains(t.Name(), "VirtualSessionActorInformation") && request.Room.UserId == "user1" { - if request.Room.Action == "" || request.Room.Action == "join" || request.Room.Action == "leave" { - assert.Equal("actor-type", request.Room.ActorType, "failed for %+v", request.Room) - assert.Equal("actor-id", request.Room.ActorId, "failed for %+v", request.Room) - } } // Allow joining any room. - response := &talk.BackendClientResponse{ + response := &BackendClientResponse{ Type: "room", - Room: &talk.BackendClientRoomResponse{ - Version: talk.BackendVersion, + Room: &BackendClientRoomResponse{ + Version: BackendVersion, RoomId: request.Room.RoomId, Properties: testRoomProperties, }, @@ -491,10 +421,11 @@ func processRoomRequest(t *testing.T, w http.ResponseWriter, r *http.Request, re data := map[string]string{ "userid": "userid-from-sessiondata", } - tmp, _ := json.Marshal(data) + tmp, err := json.Marshal(data) + require.NoError(err) response.Room.Session = tmp case "test-room-initial-permissions": - permissions := []api.Permission{api.PERMISSION_MAY_PUBLISH_AUDIO} + permissions := []Permission{PERMISSION_MAY_PUBLISH_AUDIO} response.Room.Permissions = &permissions } return response @@ -503,16 +434,15 @@ func processRoomRequest(t *testing.T, w http.ResponseWriter, r *http.Request, re var ( sessionRequestHander struct { sync.Mutex - // +checklocks:Mutex - handlers map[*testing.T]func(*talk.BackendClientSessionRequest) + handlers map[*testing.T]func(*BackendClientSessionRequest) } ) -func setSessionRequestHandler(t *testing.T, f func(*talk.BackendClientSessionRequest)) { +func setSessionRequestHandler(t *testing.T, f func(*BackendClientSessionRequest)) { sessionRequestHander.Lock() defer sessionRequestHander.Unlock() if sessionRequestHander.handlers == nil { - sessionRequestHander.handlers = make(map[*testing.T]func(*talk.BackendClientSessionRequest)) + sessionRequestHander.handlers = make(map[*testing.T]func(*BackendClientSessionRequest)) } if _, found := sessionRequestHander.handlers[t]; !found { t.Cleanup(func() { @@ -532,9 +462,9 @@ func clearSessionRequestHandler(t *testing.T) { // nolint delete(sessionRequestHander.handlers, t) } -func processSessionRequest(t *testing.T, w http.ResponseWriter, r *http.Request, request *talk.BackendClientRequest) *talk.BackendClientResponse { +func processSessionRequest(t *testing.T, w http.ResponseWriter, r *http.Request, request *BackendClientRequest) *BackendClientResponse { if request.Type != "session" || request.Session == nil { - require.Fail(t, "Expected an session backend request", "received %+v", request) + require.Fail(t, "Expected an session backend request, got %+v", request) } sessionRequestHander.Lock() @@ -543,37 +473,45 @@ func processSessionRequest(t *testing.T, w http.ResponseWriter, r *http.Request, f(request.Session) } - response := &talk.BackendClientResponse{ + response := &BackendClientResponse{ Type: "session", - Session: &talk.BackendClientSessionResponse{ - Version: talk.BackendVersion, + Session: &BackendClientSessionResponse{ + Version: BackendVersion, RoomId: request.Session.RoomId, }, } return response } -var ( - pingRequests test.Storage[[]*talk.BackendClientRequest] -) +var pingRequests map[*testing.T][]*BackendClientRequest -func getPingRequests(t *testing.T) []*talk.BackendClientRequest { - entries, _ := pingRequests.Get(t) - return entries +func getPingRequests(t *testing.T) []*BackendClientRequest { + return pingRequests[t] } func clearPingRequests(t *testing.T) { - pingRequests.Del(t) + delete(pingRequests, t) } -func storePingRequest(t *testing.T, request *talk.BackendClientRequest) { - entries, _ := pingRequests.Get(t) - pingRequests.Set(t, append(entries, request)) +func storePingRequest(t *testing.T, request *BackendClientRequest) { + if entries, found := pingRequests[t]; !found { + if pingRequests == nil { + pingRequests = make(map[*testing.T][]*BackendClientRequest) + } + pingRequests[t] = []*BackendClientRequest{ + request, + } + t.Cleanup(func() { + clearPingRequests(t) + }) + } else { + pingRequests[t] = append(entries, request) + } } -func processPingRequest(t *testing.T, w http.ResponseWriter, r *http.Request, request *talk.BackendClientRequest) *talk.BackendClientResponse { +func processPingRequest(t *testing.T, w http.ResponseWriter, r *http.Request, request *BackendClientRequest) *BackendClientResponse { if request.Type != "ping" || request.Ping == nil { - require.Fail(t, "Expected an ping backend request", "received %+v", request) + require.Fail(t, "Expected an ping backend request, got %+v", request) } if request.Ping.RoomId == "test-room-with-sessiondata" { @@ -584,30 +522,23 @@ func processPingRequest(t *testing.T, w http.ResponseWriter, r *http.Request, re storePingRequest(t, request) - response := &talk.BackendClientResponse{ + response := &BackendClientResponse{ Type: "ping", - Ping: &talk.BackendClientRingResponse{ - Version: talk.BackendVersion, + Ping: &BackendClientRingResponse{ + Version: BackendVersion, RoomId: request.Ping.RoomId, }, } return response } -type testAuthToken struct { - PrivateKey string - PublicKey string -} - -var ( - authTokens test.Storage[testAuthToken] -) - func ensureAuthTokens(t *testing.T) (string, string) { require := require.New(t) - - if tokens, found := authTokens.Get(t); found { - return tokens.PrivateKey, tokens.PublicKey + if privateKey := os.Getenv("PRIVATE_AUTH_TOKEN_" + t.Name()); privateKey != "" { + publicKey := os.Getenv("PUBLIC_AUTH_TOKEN_" + t.Name()) + // should not happen, always both keys are created + require.NotEmpty(publicKey, "public key is empty") + return privateKey, publicKey } var private []byte @@ -665,16 +596,13 @@ func ensureAuthTokens(t *testing.T) (string, string) { } privateKey := base64.StdEncoding.EncodeToString(private) + t.Setenv("PRIVATE_AUTH_TOKEN_"+t.Name(), privateKey) publicKey := base64.StdEncoding.EncodeToString(public) - - authTokens.Set(t, testAuthToken{ - PrivateKey: privateKey, - PublicKey: publicKey, - }) + t.Setenv("PUBLIC_AUTH_TOKEN_"+t.Name(), publicKey) return privateKey, publicKey } -func getPrivateAuthToken(t *testing.T) (key any) { +func getPrivateAuthToken(t *testing.T) (key interface{}) { private, _ := ensureAuthTokens(t) data, err := base64.StdEncoding.DecodeString(private) require.NoError(t, err) @@ -689,7 +617,7 @@ func getPrivateAuthToken(t *testing.T) (key any) { return key } -func getPublicAuthToken(t *testing.T) (key any) { +func getPublicAuthToken(t *testing.T) (key interface{}) { _, public := ensureAuthTokens(t) data, err := base64.StdEncoding.DecodeString(public) require.NoError(t, err) @@ -708,14 +636,8 @@ func registerBackendHandler(t *testing.T, router *mux.Router) { registerBackendHandlerUrl(t, router, "/") } -var ( - skipV2Capabilities test.Storage[bool] -) - func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { - handleFunc := validateBackendChecksum(t, func(w http.ResponseWriter, r *http.Request, request *talk.BackendClientRequest) *talk.BackendClientResponse { - assert.Regexp(t, "/ocs/v2\\.php/apps/spreed/api/v[\\d]/signaling/backend$", r.URL.Path, "invalid url for backend request %+v", request) - + handleFunc := validateBackendChecksum(t, func(w http.ResponseWriter, r *http.Request, request *BackendClientRequest) *BackendClientResponse { switch request.Type { case "auth": return processAuthRequest(t, w, r, request) @@ -726,7 +648,7 @@ func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { case "ping": return processPingRequest(t, w, r, request) default: - require.Fail(t, "Unsupported request", "received: %+v", request) + require.Fail(t, "Unsupported request received: %+v", request) return nil } }) @@ -747,21 +669,24 @@ func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { if strings.Contains(t.Name(), "Federation") { features = append(features, "federation-v2") } - signaling := api.StringMap{ + signaling := map[string]interface{}{ "foo": "bar", "baz": 42, } - config := api.StringMap{ + config := map[string]interface{}{ "signaling": signaling, } if strings.Contains(t.Name(), "MultiRoom") { - signaling[talk.ConfigKeySessionPingLimit] = 2 + signaling[ConfigKeySessionPingLimit] = 2 } - skipV2, _ := skipV2Capabilities.Get(t) - if (strings.Contains(t.Name(), "V2") && !skipV2) || strings.Contains(t.Name(), "Federation") { + useV2 := true + if os.Getenv("SKIP_V2_CAPABILITIES") != "" { + useV2 = false + } + if (strings.Contains(t.Name(), "V2") && useV2) || strings.Contains(t.Name(), "Federation") { key := getPublicAuthToken(t) public, err := x509.MarshalPKIXPublicKey(key) - assert.NoError(t, err) + require.NoError(t, err) var pemType string if strings.Contains(t.Name(), "ECDSA") { pemType = "ECDSA PUBLIC KEY" @@ -778,18 +703,17 @@ func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { if strings.Contains(t.Name(), "Ed25519_Nextcloud") { // Simulate Nextcloud which returns the Ed25519 key as base64-encoded data. encoded := base64.StdEncoding.EncodeToString(key.(ed25519.PublicKey)) - signaling[talk.ConfigKeyHelloV2TokenKey] = encoded + signaling[ConfigKeyHelloV2TokenKey] = encoded } else { - signaling[talk.ConfigKeyHelloV2TokenKey] = string(public) + signaling[ConfigKeyHelloV2TokenKey] = string(public) } } - spreedCapa, err := json.Marshal(api.StringMap{ + spreedCapa, _ := json.Marshal(map[string]interface{}{ "features": features, "config": config, }) - assert.NoError(t, err) - response := &talk.CapabilitiesResponse{ - Version: talk.CapabilitiesVersion{ + response := &CapabilitiesResponse{ + Version: CapabilitiesVersion{ Major: 20, }, Capabilities: map[string]json.RawMessage{ @@ -800,9 +724,9 @@ func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { data, err := json.Marshal(response) assert.NoError(t, err, "Could not marshal %+v", response) - var ocs talk.OcsResponse - ocs.Ocs = &talk.OcsBody{ - Meta: talk.OcsMeta{ + var ocs OcsResponse + ocs.Ocs = &OcsBody{ + Meta: OcsMeta{ Status: "ok", StatusCode: http.StatusOK, Message: http.StatusText(http.StatusOK), @@ -810,7 +734,7 @@ func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { Data: data, } data, err = json.Marshal(ocs) - assert.NoError(t, err) + require.NoError(t, err) w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(data) // nolint @@ -824,62 +748,19 @@ func registerBackendHandlerUrl(t *testing.T, router *mux.Router, url string) { } } -func Benchmark_DecodePrivateSessionIdCached(b *testing.B) { - require := require.New(b) - decodeCaches := make([]*container.LruCache[*session.SessionIdData], 0, numDecodeCaches) - for range numDecodeCaches { - decodeCaches = append(decodeCaches, container.NewLruCache[*session.SessionIdData](decodeCacheSize)) - } - backend := talk.NewCompatBackend(nil) - data := &session.SessionIdData{ - Sid: 1, - Created: time.Now().UnixMicro(), - BackendId: backend.Id(), - } - codec, err := session.NewSessionIdCodec([]byte("12345678901234567890123456789012"), []byte("09876543210987654321098765432109")) - require.NoError(err) - sid, err := codec.EncodePrivate(data) - require.NoError(err, "could not create session id") - hub := &Hub{ - sessionIds: codec, - decodeCaches: decodeCaches, - } - // Decode once to populate cache. - require.NotNil(hub.decodePrivateSessionId(sid)) - for b.Loop() { - hub.decodePrivateSessionId(sid) - } -} - -func Benchmark_DecodePublicSessionIdCached(b *testing.B) { - require := require.New(b) - decodeCaches := make([]*container.LruCache[*session.SessionIdData], 0, numDecodeCaches) - for range numDecodeCaches { - decodeCaches = append(decodeCaches, container.NewLruCache[*session.SessionIdData](decodeCacheSize)) - } - backend := talk.NewCompatBackend(nil) - data := &session.SessionIdData{ - Sid: 1, - Created: time.Now().UnixMicro(), - BackendId: backend.Id(), - } - codec, err := session.NewSessionIdCodec([]byte("12345678901234567890123456789012"), []byte("09876543210987654321098765432109")) - require.NoError(err) - sid, err := codec.EncodePublic(data) - require.NoError(err, "could not create session id") - hub := &Hub{ - sessionIds: codec, - decodeCaches: decodeCaches, - } - // Decode once to populate cache. - require.NotNil(hub.decodePublicSessionId(sid)) - for b.Loop() { - hub.decodePublicSessionId(sid) - } +func performHousekeeping(hub *Hub, now time.Time) *sync.WaitGroup { + var wg sync.WaitGroup + wg.Add(1) + go func() { + hub.performHousekeeping(now) + wg.Done() + }() + return &wg } func TestWebsocketFeatures(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, server := CreateHubForTest(t) @@ -887,7 +768,7 @@ func TestWebsocketFeatures(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - conn, response, err := testClientDialer.DialContext(ctx, getWebsocketUrl(server.URL), nil) + conn, response, err := websocket.DefaultDialer.DialContext(ctx, getWebsocketUrl(server.URL), nil) require.NoError(err) defer conn.Close() // nolint @@ -895,22 +776,27 @@ func TestWebsocketFeatures(t *testing.T) { assert.True(strings.HasPrefix(serverHeader, "nextcloud-spreed-signaling/"), "expected valid server header, got \"%s\"", serverHeader) features := response.Header.Get("X-Spreed-Signaling-Features") featuresList := make(map[string]bool) - for f := range internal.SplitEntries(features, ",") { - _, found := featuresList[f] - assert.False(found, "duplicate feature id \"%s\" in \"%s\"", f, features) - featuresList[f] = true + for _, f := range strings.Split(features, ",") { + f = strings.TrimSpace(f) + if f != "" { + _, found := featuresList[f] + assert.False(found, "duplicate feature id \"%s\" in \"%s\"", f, features) + featuresList[f] = true + } } if len(featuresList) <= 1 { - assert.Fail("expected valid features header", "received \"%s\"", features) + assert.Fail("expected valid features header, got \"%s\"", features) } _, found := featuresList["hello-v2"] - assert.True(found, "expected feature \"hello-v2\"", "received \"%s\"", features) + assert.True(found, "expected feature \"hello-v2\", got \"%s\"", features) assert.NoError(conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Time{})) } func TestInitialWelcome(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -920,17 +806,20 @@ func TestInitialWelcome(t *testing.T) { client := NewTestClientContext(ctx, t, server, hub) defer client.CloseWithBye() - if msg, ok := client.RunUntilMessage(ctx); ok { - assert.Equal("welcome", msg.Type, "%+v", msg) - if assert.NotNil(msg.Welcome, "%+v", msg) { - assert.NotEmpty(msg.Welcome.Version, "%+v", msg) - assert.NotEmpty(msg.Welcome.Features, "%+v", msg) - } + msg, err := client.RunUntilMessage(ctx) + require.NoError(err) + + assert.Equal("welcome", msg.Type, "%+v", msg) + if assert.NotNil(msg.Welcome, "%+v", msg) { + assert.NotEmpty(msg.Welcome.Version, "%+v", msg) + assert.NotEmpty(msg.Welcome.Features, "%+v", msg) } } func TestExpectClientHello(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -941,22 +830,27 @@ func TestExpectClientHello(t *testing.T) { // Perform housekeeping in the future, this will cause the connection to // be terminated due to the missing "Hello" request. - hub.performHousekeeping(time.Now().Add(initialHelloTimeout + time.Second)) + performHousekeeping(hub, time.Now().Add(initialHelloTimeout+time.Second)) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() + message, err := client.RunUntilMessage(ctx) + require.NoError(checkUnexpectedClose(err)) - if message, ok := client.RunUntilMessage(ctx); ok { - if checkMessageType(t, message, "bye") { - assert.Equal("hello_timeout", message.Bye.Reason, "%+v", message.Bye) - } + message2, err := client.RunUntilMessage(ctx) + if message2 != nil { + require.Fail("Received multiple messages, already have %+v, also got %+v", message, message2) } + require.NoError(checkUnexpectedClose(err)) - client.RunUntilClosed(ctx) + if err := checkMessageType(message, "bye"); assert.NoError(err) { + assert.Equal("hello_timeout", message.Bye.Reason, "%+v", message.Bye) + } } func TestExpectClientHelloUnsupportedVersion(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -972,32 +866,39 @@ func TestExpectClientHelloUnsupportedVersion(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - if message, ok := client.RunUntilMessage(ctx); ok { - if checkMessageType(t, message, "error") { - assert.Equal("invalid_hello_version", message.Error.Code) - } + message, err := client.RunUntilMessage(ctx) + require.NoError(checkUnexpectedClose(err)) + + if err := checkMessageType(message, "error"); assert.NoError(err) { + assert.Equal("invalid_hello_version", message.Error.Code) } } func TestClientHelloV1(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - _, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello) - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello) + if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { + assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello) + assert.NotEmpty(hello.Hello.SessionId, "%+v", hello) + } } func TestClientHelloV2(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, algo := range testHelloV2Algorithms { t.Run(algo, func(t *testing.T) { - t.Parallel() require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -1010,32 +911,31 @@ func TestClientHelloV2(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - if hello, ok := client.RunUntilHello(ctx); ok { - assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) - data := hub.decodePublicSessionId(hello.Hello.SessionId) - require.NotNil(data, "Could not decode session id: %s", hello.Hello.SessionId) + data := hub.decodePublicSessionId(hello.Hello.SessionId) + require.NotNil(data, "Could not decode session id: %s", hello.Hello.SessionId) - hub.mu.RLock() - session := hub.sessions[data.Sid] - hub.mu.RUnlock() - require.NotNil(session, "Could not get session for id %+v", data) + hub.mu.RLock() + session := hub.sessions[data.Sid] + hub.mu.RUnlock() + require.NotNil(session, "Could not get session for id %+v", data) - var userdata map[string]string - require.NoError(json.Unmarshal(session.UserData(), &userdata)) + var userdata map[string]string + require.NoError(json.Unmarshal(session.UserData(), &userdata)) - assert.Equal("Displayname "+testDefaultUserId, userdata["displayname"]) - } + assert.Equal("Displayname "+testDefaultUserId, userdata["displayname"]) }) } } func TestClientHelloV2_IssuedInFuture(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, algo := range testHelloV2Algorithms { t.Run(algo, func(t *testing.T) { - t.Parallel() require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -1050,20 +950,20 @@ func TestClientHelloV2_IssuedInFuture(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - if hello, ok := client.RunUntilHello(ctx); ok { - assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) - } + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) }) } } func TestClientHelloV2_IssuedFarInFuture(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, algo := range testHelloV2Algorithms { t.Run(algo, func(t *testing.T) { - t.Parallel() require := require.New(t) + assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) client := NewTestClient(t, server, hub) @@ -1076,17 +976,22 @@ func TestClientHelloV2_IssuedFarInFuture(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client.RunUntilError(ctx, "token_not_valid_yet") // nolint + message, err := client.RunUntilMessage(ctx) + require.NoError(checkUnexpectedClose(err)) + + if err := checkMessageType(message, "error"); assert.NoError(err) { + assert.Equal("token_not_valid_yet", message.Error.Code, "%+v", message) + } }) } } func TestClientHelloV2_Expired(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, algo := range testHelloV2Algorithms { t.Run(algo, func(t *testing.T) { - t.Parallel() require := require.New(t) + assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) client := NewTestClient(t, server, hub) @@ -1099,17 +1004,22 @@ func TestClientHelloV2_Expired(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client.RunUntilError(ctx, "token_expired") // nolint + message, err := client.RunUntilMessage(ctx) + require.NoError(checkUnexpectedClose(err)) + + if err := checkMessageType(message, "error"); assert.NoError(err) { + assert.Equal("token_expired", message.Error.Code, "%+v", message) + } }) } } func TestClientHelloV2_IssuedAtMissing(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, algo := range testHelloV2Algorithms { t.Run(algo, func(t *testing.T) { - t.Parallel() require := require.New(t) + assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) client := NewTestClient(t, server, hub) @@ -1122,17 +1032,22 @@ func TestClientHelloV2_IssuedAtMissing(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client.RunUntilError(ctx, "token_not_valid_yet") // nolint + message, err := client.RunUntilMessage(ctx) + require.NoError(checkUnexpectedClose(err)) + + if err := checkMessageType(message, "error"); assert.NoError(err) { + assert.Equal("token_not_valid_yet", message.Error.Code, "%+v", message) + } }) } } func TestClientHelloV2_ExpiresAtMissing(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, algo := range testHelloV2Algorithms { t.Run(algo, func(t *testing.T) { - t.Parallel() require := require.New(t) + assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) client := NewTestClient(t, server, hub) @@ -1145,16 +1060,20 @@ func TestClientHelloV2_ExpiresAtMissing(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client.RunUntilError(ctx, "token_expired") // nolint + message, err := client.RunUntilMessage(ctx) + require.NoError(checkUnexpectedClose(err)) + + if err := checkMessageType(message, "error"); assert.NoError(err) { + assert.Equal("token_expired", message.Error.Code, "%+v", message) + } }) } } func TestClientHelloV2_CachedCapabilities(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, algo := range testHelloV2Algorithms { t.Run(algo, func(t *testing.T) { - t.Parallel() require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -1163,26 +1082,28 @@ func TestClientHelloV2_CachedCapabilities(t *testing.T) { defer cancel() // Simulate old-style Nextcloud without capabilities for Hello V2. - skipV2Capabilities.Set(t, true) + t.Setenv("SKIP_V2_CAPABILITIES", "1") client1 := NewTestClient(t, server, hub) defer client1.CloseWithBye() require.NoError(client1.SendHelloV1(testDefaultUserId + "1")) - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId+"1", hello1.Hello.UserId, "%+v", hello1.Hello) assert.NotEmpty(hello1.Hello.SessionId, "%+v", hello1.Hello) // Simulate updated Nextcloud with capabilities for Hello V2. - skipV2Capabilities.Set(t, false) + t.Setenv("SKIP_V2_CAPABILITIES", "") client2 := NewTestClient(t, server, hub) defer client2.CloseWithBye() require.NoError(client2.SendHelloV2(testDefaultUserId + "2")) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId+"2", hello2.Hello.UserId, "%+v", hello2.Hello) assert.NotEmpty(hello2.Hello.SessionId, "%+v", hello2.Hello) }) @@ -1191,21 +1112,30 @@ func TestClientHelloV2_CachedCapabilities(t *testing.T) { func TestClientHelloWithSpaces(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + userId := "test user with spaces" + require.NoError(client.SendHello(userId)) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - _, hello := NewTestClientWithHello(ctx, t, server, hub, userId) - assert.Equal(userId, hello.Hello.UserId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) + if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { + assert.Equal(userId, hello.Hello.UserId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) + } } func TestClientHelloAllowAll(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTestWithConfig(t, func(server *httptest.Server) (*goconf.ConfigFile, error) { config, err := getTestConfig(server) @@ -1218,16 +1148,22 @@ func TestClientHelloAllowAll(t *testing.T) { return config, nil }) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - _, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) + if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { + assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) + } } func TestClientHelloSessionLimit(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -1306,12 +1242,12 @@ func TestClientHelloSessionLimit(t *testing.T) { params1 := TestBackendClientAuthParams{ UserId: testDefaultUserId, } - require.NoError(client.SendHelloParams(server1.URL+"/one", api.HelloVersionV1, "client", nil, params1)) + require.NoError(client.SendHelloParams(server1.URL+"/one", HelloVersionV1, "client", nil, params1)) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - if hello, ok := client.RunUntilHello(ctx); ok { + if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) } @@ -1323,14 +1259,19 @@ func TestClientHelloSessionLimit(t *testing.T) { params2 := TestBackendClientAuthParams{ UserId: testDefaultUserId + "2", } - require.NoError(client2.SendHelloParams(server1.URL+"/one", api.HelloVersionV1, "client", nil, params2)) + require.NoError(client2.SendHelloParams(server1.URL+"/one", HelloVersionV1, "client", nil, params2)) - client2.RunUntilError(ctx, "session_limit_exceeded") //nolint + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("error", msg.Type, "%+v", msg) + if assert.NotNil(msg.Error, "%+v", msg) { + assert.Equal("session_limit_exceeded", msg.Error.Code, "%+v", msg) + } + } // The client can connect to a different backend. - require.NoError(client2.SendHelloParams(server1.URL+"/two", api.HelloVersionV1, "client", nil, params2)) + require.NoError(client2.SendHelloParams(server1.URL+"/two", HelloVersionV1, "client", nil, params2)) - if hello, ok := client2.RunUntilHello(ctx); ok { + if hello, err := client2.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId+"2", hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) } @@ -1345,9 +1286,9 @@ func TestClientHelloSessionLimit(t *testing.T) { params3 := TestBackendClientAuthParams{ UserId: testDefaultUserId + "3", } - require.NoError(client3.SendHelloParams(server1.URL+"/one", api.HelloVersionV1, "client", nil, params3)) + require.NoError(client3.SendHelloParams(server1.URL+"/one", HelloVersionV1, "client", nil, params3)) - if hello, ok := client3.RunUntilHello(ctx); ok { + if hello, err := client3.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId+"3", hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) } @@ -1357,49 +1298,46 @@ func TestClientHelloSessionLimit(t *testing.T) { func TestSessionIdsUnordered(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) - var mu sync.Mutex - var publicSessionIds []api.PublicSessionId - var wg sync.WaitGroup - for range 20 { - wg.Go(func() { - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() + publicSessionIds := make([]string, 0) + for i := 0; i < 20; i++ { + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() - _, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) // nolint:testifylint + require.NoError(client.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) data := hub.decodePublicSessionId(hello.Hello.SessionId) if !assert.NotNil(data, "Could not decode session id: %s", hello.Hello.SessionId) { - return + break } hub.mu.RLock() session := hub.sessions[data.Sid] hub.mu.RUnlock() if !assert.NotNil(session, "Could not get session for id %+v", data) { - return + break } - mu.Lock() publicSessionIds = append(publicSessionIds, session.PublicId()) - mu.Unlock() - }) + } } - wg.Wait() - - mu.Lock() - defer mu.Unlock() require.NotEmpty(publicSessionIds, "no session ids decoded") larger := 0 smaller := 0 - var prevSid api.PublicSessionId + prevSid := "" for i, sid := range publicSessionIds { if i > 0 { if sid > prevSid { @@ -1420,14 +1358,21 @@ func TestSessionIdsUnordered(t *testing.T) { func TestClientHelloResume(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) require.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1439,31 +1384,16 @@ func TestClientHelloResume(t *testing.T) { defer client.CloseWithBye() require.NoError(client.SendHelloResume(hello.Hello.ResumeId)) - if hello2, ok := client.RunUntilHello(ctx); ok { + if hello2, err := client.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId, "%+v", hello2.Hello) assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId, "%+v", hello2.Hello) } } -type throttlerTiming struct { - t *testing.T - - now time.Time - expectedSleep time.Duration -} - -func (t *throttlerTiming) getNow() time.Time { - return t.now -} - -func (t *throttlerTiming) doDelay(ctx context.Context, duration time.Duration) { - t.t.Helper() - assert.Equal(t.t, t.expectedSleep, duration) -} - func TestClientHelloResumeThrottle(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -1472,12 +1402,10 @@ func TestClientHelloResumeThrottle(t *testing.T) { t: t, now: time.Now(), } - throttler, err := async.NewCustomMemoryThrottler(timing.getNow, timing.doDelay) - require.NoError(err) - t.Cleanup(func() { - throttler.Close() - }) - hub.throttler = throttler + th := newMemoryThrottlerForTest(t) + th.getNow = timing.getNow + th.doDelay = timing.doDelay + hub.throttler = th ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -1488,9 +1416,20 @@ func TestClientHelloResumeThrottle(t *testing.T) { timing.expectedSleep = 100 * time.Millisecond require.NoError(client.SendHelloResume("this-is-invalid")) - client.RunUntilError(ctx, "no_such_session") //nolint + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("error", msg.Type, "%+v", msg) + if assert.NotNil(msg.Error, "%+v", msg) { + assert.Equal("no_such_session", msg.Error.Code, "%+v", msg) + } + } - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + client = NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId) assert.NotEmpty(hello.Hello.SessionId) assert.NotEmpty(hello.Hello.ResumeId) @@ -1500,7 +1439,7 @@ func TestClientHelloResumeThrottle(t *testing.T) { // Perform housekeeping in the future, this will cause the session to be // cleaned up after it is expired. - hub.performHousekeeping(time.Now().Add(sessionExpireDuration + time.Second)) + performHousekeeping(hub, time.Now().Add(sessionExpireDuration+time.Second)).Wait() client = NewTestClient(t, server, hub) defer client.CloseWithBye() @@ -1508,7 +1447,12 @@ func TestClientHelloResumeThrottle(t *testing.T) { // Valid but expired resume ids will not be throttled. timing.expectedSleep = 0 * time.Millisecond require.NoError(client.SendHelloResume(hello.Hello.ResumeId)) - client.RunUntilError(ctx, "no_such_session") //nolint + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("error", msg.Type, "%+v", msg) + if assert.NotNil(msg.Error, "%+v", msg) { + assert.Equal("no_such_session", msg.Error.Code, "%+v", msg) + } + } client = NewTestClient(t, server, hub) defer client.CloseWithBye() @@ -1516,18 +1460,31 @@ func TestClientHelloResumeThrottle(t *testing.T) { timing.expectedSleep = 200 * time.Millisecond require.NoError(client.SendHelloResume("this-is-invalid")) - client.RunUntilError(ctx, "no_such_session") //nolint + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("error", msg.Type, "%+v", msg) + if assert.NotNil(msg.Error, "%+v", msg) { + assert.Equal("no_such_session", msg.Error.Code, "%+v", msg) + } + } } func TestClientHelloResumeExpired(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1537,26 +1494,37 @@ func TestClientHelloResumeExpired(t *testing.T) { // Perform housekeeping in the future, this will cause the session to be // cleaned up after it is expired. - hub.performHousekeeping(time.Now().Add(sessionExpireDuration + time.Second)) + performHousekeeping(hub, time.Now().Add(sessionExpireDuration+time.Second)).Wait() client = NewTestClient(t, server, hub) defer client.CloseWithBye() - if assert.NoError(client.SendHelloResume(hello.Hello.ResumeId)) { - client.RunUntilError(ctx, "no_such_session") //nolint + require.NoError(client.SendHelloResume(hello.Hello.ResumeId)) + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("error", msg.Type, "%+v", msg) + if assert.NotNil(msg.Error, "%+v", msg) { + assert.Equal("no_such_session", msg.Error.Code, "%+v", msg) + } } } func TestClientHelloResumeTakeover(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client1.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) require.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1565,32 +1533,44 @@ func TestClientHelloResumeTakeover(t *testing.T) { defer client2.CloseWithBye() require.NoError(client2.SendHelloResume(hello.Hello.ResumeId)) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId, "%+v", hello2.Hello) assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId, "%+v", hello2.Hello) // The first client got disconnected with a reason in a "Bye" message. - if msg, ok := client1.RunUntilMessage(ctx); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal("bye", msg.Type, "%+v", msg) if assert.NotNil(msg.Bye, "%+v", msg) { assert.Equal("session_resumed", msg.Bye.Reason, "%+v", msg) } } - client1.RunUntilClosed(ctx) + if msg, err := client1.RunUntilMessage(ctx); err == nil { + assert.Fail("Expected error but received %+v", msg) + } else if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + assert.Fail("Expected close error but received %+v", err) + } } func TestClientHelloResumeOtherHub(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1615,7 +1595,13 @@ func TestClientHelloResumeOtherHub(t *testing.T) { assert.Equal(0, count, "Should have removed all sessions") // The new client will get the same (internal) sid for his session. - _, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + newClient := NewTestClient(t, server, hub) + defer newClient.CloseWithBye() + + require.NoError(newClient.SendHello(testDefaultUserId)) + + hello2, err := newClient.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) assert.NotEmpty(hello2.Hello.SessionId, "%+v", hello2.Hello) assert.NotEmpty(hello2.Hello.ResumeId, "%+v", hello2.Hello) @@ -1624,7 +1610,12 @@ func TestClientHelloResumeOtherHub(t *testing.T) { client = NewTestClient(t, server, hub) defer client.CloseWithBye() require.NoError(client.SendHelloResume(hello.Hello.ResumeId)) - client.RunUntilError(ctx, "no_such_session") //nolint + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("error", msg.Type, "%+v", msg) + if assert.NotNil(msg.Error, "%+v", msg) { + assert.Equal("no_such_session", msg.Error.Code, "%+v", msg) + } + } // Expire old sessions hub.performHousekeeping(time.Now().Add(2 * sessionExpireDuration)) @@ -1632,19 +1623,30 @@ func TestClientHelloResumeOtherHub(t *testing.T) { func TestClientHelloResumePublicId(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) // Test that a client can't resume a "public" session of another user. hub, _, _, server := CreateHubForTest(t) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, } @@ -1653,8 +1655,8 @@ func TestClientHelloResumePublicId(t *testing.T) { client1.SendMessage(recipient2, data) // nolint var payload string - var sender *api.MessageServerMessageSender - if checkReceiveClientMessageWithSender(ctx, t, client2, "session", hello1.Hello, &payload, &sender) { + var sender *MessageServerMessageSender + if err := checkReceiveClientMessageWithSender(ctx, client2, "session", hello1.Hello, &payload, &sender); assert.NoError(err) { assert.Equal(data, payload) } @@ -1665,8 +1667,13 @@ func TestClientHelloResumePublicId(t *testing.T) { defer client1.CloseWithBye() // Can't resume a session with the id received from messages of a client. - require.NoError(client1.SendHelloResume(api.PrivateSessionId(sender.SessionId))) - client1.RunUntilError(ctx, "no_such_session") // nolint + require.NoError(client1.SendHelloResume(sender.SessionId)) + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("error", msg.Type, "%+v", msg) + if assert.NotNil(msg.Error, "%+v", msg) { + assert.Equal("no_such_session", msg.Error.Code, "%+v", msg) + } + } // Expire old sessions hub.performHousekeeping(time.Now().Add(2 * sessionExpireDuration)) @@ -1674,21 +1681,28 @@ func TestClientHelloResumePublicId(t *testing.T) { func TestClientHelloByeResume(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) require.NoError(client.SendBye()) - if message, ok := client.RunUntilMessage(ctx); ok { - checkMessageType(t, message, "bye") + if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(checkMessageType(message, "bye")) } client.Close() @@ -1699,19 +1713,31 @@ func TestClientHelloByeResume(t *testing.T) { defer client.CloseWithBye() require.NoError(client.SendHelloResume(hello.Hello.ResumeId)) - client.RunUntilError(ctx, "no_such_session") //nolint + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.Equal("error", msg.Type, "%+v", msg) + if assert.NotNil(msg.Error, "%+v", msg) { + assert.Equal("no_such_session", msg.Error.Code, "%+v", msg) + } + } } func TestClientHelloResumeAndJoin(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1723,17 +1749,17 @@ func TestClientHelloResumeAndJoin(t *testing.T) { defer client.CloseWithBye() require.NoError(client.SendHelloResume(hello.Hello.ResumeId)) - if hello2, ok := client.RunUntilHello(ctx); ok && hello != nil { - assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) - assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId, "%+v", hello2.Hello) - assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId, "%+v", hello2.Hello) - } + hello2, err := client.RunUntilHello(ctx) + require.NoError(err) + assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) + assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId, "%+v", hello2.Hello) + assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId, "%+v", hello2.Hello) // Join room by id. roomId := "test-room" - if roomMsg, ok := client.JoinRoom(ctx, roomId); ok { - assert.Equal(roomId, roomMsg.Room.RoomId) - } + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) } func runGrpcProxyTest(t *testing.T, f func(hub1, hub2 *Hub, server1, server2 *httptest.Server)) { @@ -1774,15 +1800,22 @@ func runGrpcProxyTest(t *testing.T, f func(hub1, hub2 *Hub, server1, server2 *ht f(hub1, hub2, server1, server2) } -func TestClientHelloResumeProxy(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { +func TestClientHelloResumeProxy(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { runGrpcProxyTest(t, func(hub1, hub2 *Hub, server1, server2 *httptest.Server) { require := require.New(t) assert := assert.New(t) + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId) + hello, err := client1.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1794,45 +1827,54 @@ func TestClientHelloResumeProxy(t *testing.T) { // nolint:paralleltest defer client2.CloseWithBye() require.NoError(client2.SendHelloResume(hello.Hello.ResumeId)) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId, "%+v", hello2.Hello) assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId, "%+v", hello2.Hello) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err := client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // We will receive a "joined" event. - client2.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello.Hello)) room := hub1.getRoom(roomId) require.NotNil(room, "Could not find room %s", roomId) room2 := hub2.getRoom(roomId) require.Nil(room2, "Should not have gotten room %s", roomId) - users := []api.StringMap{ + users := []map[string]interface{}{ { "sessionId": "the-session-id", "inCall": 1, }, } room.PublishUsersInCallChanged(users, users) - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) }) }) } -func TestClientHelloResumeProxy_Takeover(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { +func TestClientHelloResumeProxy_Takeover(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { runGrpcProxyTest(t, func(hub1, hub2 *Hub, server1, server2 *httptest.Server) { require := require.New(t) assert := assert.New(t) + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId) + hello, err := client1.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) require.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1841,52 +1883,69 @@ func TestClientHelloResumeProxy_Takeover(t *testing.T) { // nolint:paralleltest defer client2.CloseWithBye() require.NoError(client2.SendHelloResume(hello.Hello.ResumeId)) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId, "%+v", hello2.Hello) assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId, "%+v", hello2.Hello) // The first client got disconnected with a reason in a "Bye" message. - if msg, ok := client1.RunUntilMessage(ctx); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal("bye", msg.Type, "%+v", msg) if assert.NotNil(msg.Bye, "%+v", msg) { assert.Equal("session_resumed", msg.Bye.Reason, "%+v", msg) } } - client1.RunUntilClosed(ctx) + if msg, err := client1.RunUntilMessage(ctx); err == nil { + assert.Fail("Expected error but received %+v", msg) + } else if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + assert.Fail("Expected close error but received %+v", err) + } client3 := NewTestClient(t, server1, hub1) defer client3.CloseWithBye() require.NoError(client3.SendHelloResume(hello.Hello.ResumeId)) - hello3 := MustSucceed1(t, client3.RunUntilHello, ctx) + hello3, err := client3.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello3.Hello.UserId, "%+v", hello3.Hello) assert.Equal(hello.Hello.SessionId, hello3.Hello.SessionId, "%+v", hello3.Hello) assert.Equal(hello.Hello.ResumeId, hello3.Hello.ResumeId, "%+v", hello3.Hello) // The second client got disconnected with a reason in a "Bye" message. - if msg, ok := client2.RunUntilMessage(ctx); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal("bye", msg.Type, "%+v", msg) if assert.NotNil(msg.Bye, "%+v", msg) { assert.Equal("session_resumed", msg.Bye.Reason, "%+v", msg) } } - client2.RunUntilClosed(ctx) + if msg, err := client2.RunUntilMessage(ctx); err == nil { + assert.Fail("Expected error but received %+v", msg) + } else if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + assert.Fail("Expected close error but received %+v", err) + } }) }) } -func TestClientHelloResumeProxy_Disconnect(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { +func TestClientHelloResumeProxy_Disconnect(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { runGrpcProxyTest(t, func(hub1, hub2 *Hub, server1, server2 *httptest.Server) { require := require.New(t) assert := assert.New(t) + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId) + hello, err := client1.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1898,13 +1957,14 @@ func TestClientHelloResumeProxy_Disconnect(t *testing.T) { // nolint:paralleltes defer client2.CloseWithBye() require.NoError(client2.SendHelloResume(hello.Hello.ResumeId)) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId, "%+v", hello2.Hello) assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId, "%+v", hello2.Hello) // Simulate unclean shutdown of second instance. - hub2.rpcServer.CloseUnclean() + hub2.rpcServer.conn.Stop() assert.NoError(client2.WaitForClientRemoved(ctx)) }) @@ -1913,20 +1973,29 @@ func TestClientHelloResumeProxy_Disconnect(t *testing.T) { // nolint:paralleltes func TestClientHelloClient(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHelloClient(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - _, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) + if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { + assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) + } } func TestClientHelloClient_V3Api(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -1939,12 +2008,12 @@ func TestClientHelloClient_V3Api(t *testing.T) { } // The "/api/v1/signaling/" URL will be changed to use "v3" as the "signaling-v3" // feature is returned by the capabilities endpoint. - require.NoError(client.SendHelloParams(server.URL, api.HelloVersionV1, "client", nil, params)) + require.NoError(client.SendHelloParams(server.URL+"/ocs/v2.php/apps/spreed/api/v1/signaling/backend", HelloVersionV1, "client", nil, params)) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - if hello, ok := client.RunUntilHello(ctx); ok { + if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId, hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1953,6 +2022,7 @@ func TestClientHelloClient_V3Api(t *testing.T) { func TestClientHelloInternal(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -1965,7 +2035,7 @@ func TestClientHelloInternal(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - if hello, ok := client.RunUntilHello(ctx); ok { + if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { assert.Empty(hello.Hello.UserId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) @@ -1973,7 +2043,7 @@ func TestClientHelloInternal(t *testing.T) { } func TestClientMessageToSessionId(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -1993,35 +2063,43 @@ func TestClientMessageToSessionId(t *testing.T) { hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) } - mcu1 := sfutest.NewSFU(t) + mcu1, err := NewTestMCU() + require.NoError(err) hub1.SetMcu(mcu1) if hub1 != hub2 { - mcu2 := sfutest.NewSFU(t) + mcu2, err := NewTestMCU() + require.NoError(err) hub2.SetMcu(mcu2) } + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) - // Make sure the session subscription events are processed. - eventstest.WaitForAsyncEventsFlushed(ctx, t, hub1.events) - eventstest.WaitForAsyncEventsFlushed(ctx, t, hub2.events) - - recipient1 := api.MessageClientMessageRecipient{ + recipient1 := MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, } - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, } - data1 := api.StringMap{ + data1 := map[string]interface{}{ "type": "test", "message": "from-1-to-2", } @@ -2030,11 +2108,11 @@ func TestClientMessageToSessionId(t *testing.T) { client2.SendMessage(recipient1, data2) // nolint var payload1 string - if checkReceiveClientMessage(ctx, t, client1, "session", hello2.Hello, &payload1) { + if err := checkReceiveClientMessage(ctx, client1, "session", hello2.Hello, &payload1); assert.NoError(err) { assert.Equal(data2, payload1) } - var payload2 api.StringMap - if checkReceiveClientMessage(ctx, t, client2, "session", hello1.Hello, &payload2) { + var payload2 map[string]interface{} + if err := checkReceiveClientMessage(ctx, client2, "session", hello1.Hello, &payload2); assert.NoError(err) { assert.Equal(data1, payload2) } }) @@ -2042,7 +2120,7 @@ func TestClientMessageToSessionId(t *testing.T) { } func TestClientControlToSessionId(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -2062,22 +2140,28 @@ func TestClientControlToSessionId(t *testing.T) { hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) } + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) - // Make sure the session subscription events are processed. - eventstest.WaitForAsyncEventsFlushed(ctx, t, hub1.events) - eventstest.WaitForAsyncEventsFlushed(ctx, t, hub2.events) - - recipient1 := api.MessageClientMessageRecipient{ + recipient1 := MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, } - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, } @@ -2088,10 +2172,10 @@ func TestClientControlToSessionId(t *testing.T) { client2.SendControl(recipient1, data2) // nolint var payload string - if checkReceiveClientControl(ctx, t, client1, "session", hello2.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client1, "session", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } - if checkReceiveClientControl(ctx, t, client2, "session", hello1.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client2, "session", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } }) @@ -2100,15 +2184,26 @@ func TestClientControlToSessionId(t *testing.T) { func TestClientControlMissingPermissions(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) session1 := hub.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession) @@ -2117,22 +2212,22 @@ func TestClientControlMissingPermissions(t *testing.T) { require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) // Client 1 may not send control messages (will be ignored). - session1.SetPermissions([]api.Permission{ - api.PERMISSION_MAY_PUBLISH_AUDIO, - api.PERMISSION_MAY_PUBLISH_VIDEO, + session1.SetPermissions([]Permission{ + PERMISSION_MAY_PUBLISH_AUDIO, + PERMISSION_MAY_PUBLISH_VIDEO, }) // Client 2 may send control messages. - session2.SetPermissions([]api.Permission{ - api.PERMISSION_MAY_PUBLISH_AUDIO, - api.PERMISSION_MAY_PUBLISH_VIDEO, - api.PERMISSION_MAY_CONTROL, + session2.SetPermissions([]Permission{ + PERMISSION_MAY_PUBLISH_AUDIO, + PERMISSION_MAY_PUBLISH_VIDEO, + PERMISSION_MAY_CONTROL, }) - recipient1 := api.MessageClientMessageRecipient{ + recipient1 := MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, } - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, } @@ -2143,35 +2238,50 @@ func TestClientControlMissingPermissions(t *testing.T) { client2.SendControl(recipient1, data2) // nolint var payload string - if checkReceiveClientControl(ctx, t, client1, "session", hello2.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client1, "session", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, context.DeadlineExceeded) + if err := checkReceiveClientMessage(ctx2, client2, "session", hello1.Hello, &payload); err == nil { + assert.Fail("Expected no payload, got %+v", payload) + } else { + assert.ErrorIs(err, ErrNoMessageReceived) + } } func TestClientMessageToUserId(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) require.NotEqual(hello1.Hello.UserId, hello2.Hello.UserId) - recipient1 := api.MessageClientMessageRecipient{ + recipient1 := MessageClientMessageRecipient{ Type: "user", UserId: hello1.Hello.UserId, } - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "user", UserId: hello2.Hello.UserId, } @@ -2182,34 +2292,45 @@ func TestClientMessageToUserId(t *testing.T) { client2.SendMessage(recipient1, data2) // nolint var payload string - if checkReceiveClientMessage(ctx, t, client1, "user", hello2.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client1, "user", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } - if checkReceiveClientMessage(ctx, t, client2, "user", hello1.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client2, "user", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } } func TestClientControlToUserId(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) require.NotEqual(hello1.Hello.UserId, hello2.Hello.UserId) - recipient1 := api.MessageClientMessageRecipient{ + recipient1 := MessageClientMessageRecipient{ Type: "user", UserId: hello1.Hello.UserId, } - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "user", UserId: hello2.Hello.UserId, } @@ -2220,27 +2341,41 @@ func TestClientControlToUserId(t *testing.T) { client2.SendControl(recipient1, data2) // nolint var payload string - if checkReceiveClientControl(ctx, t, client1, "user", hello2.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client1, "user", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } - if checkReceiveClientControl(ctx, t, client2, "user", hello1.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client2, "user", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } } func TestClientMessageToUserIdMultipleSessions(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2a := NewTestClient(t, server, hub) + defer client2a.CloseWithBye() + require.NoError(client2a.SendHello(testDefaultUserId + "2")) + client2b := NewTestClient(t, server, hub) + defer client2b.CloseWithBye() + require.NoError(client2b.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2a, hello2a := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - client2b, hello2b := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2a, err := client2a.RunUntilHello(ctx) + require.NoError(err) + hello2b, err := client2b.RunUntilHello(ctx) + require.NoError(err) require.NotEqual(hello1.Hello.SessionId, hello2a.Hello.SessionId) require.NotEqual(hello1.Hello.SessionId, hello2b.Hello.SessionId) @@ -2250,7 +2385,7 @@ func TestClientMessageToUserIdMultipleSessions(t *testing.T) { require.NotEqual(hello1.Hello.UserId, hello2b.Hello.UserId) require.Equal(hello2a.Hello.UserId, hello2b.Hello.UserId) - recipient := api.MessageClientMessageRecipient{ + recipient := MessageClientMessageRecipient{ Type: "user", UserId: hello2a.Hello.UserId, } @@ -2260,16 +2395,23 @@ func TestClientMessageToUserIdMultipleSessions(t *testing.T) { // Both clients will receive the message as it was sent to the user. var payload string - if checkReceiveClientMessage(ctx, t, client2a, "user", hello1.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client2a, "user", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } - if checkReceiveClientMessage(ctx, t, client2b, "user", hello1.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client2b, "user", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } } +func WaitForUsersJoined(ctx context.Context, t *testing.T, client1 *TestClient, hello1 *ServerMessage, client2 *TestClient, hello2 *ServerMessage) { + // We will receive "joined" events for all clients. The ordering is not + // defined as messages are processed and sent by asynchronous event handlers. + assert.NoError(t, client1.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + assert.NoError(t, client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) +} + func TestClientMessageToRoom(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -2292,25 +2434,37 @@ func TestClientMessageToRoom(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) require.NotEqual(hello1.Hello.UserId, hello2.Hello.UserId) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Give message processing some time. time.Sleep(10 * time.Millisecond) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) - recipient := api.MessageClientMessageRecipient{ + recipient := MessageClientMessageRecipient{ Type: "room", } @@ -2320,11 +2474,11 @@ func TestClientMessageToRoom(t *testing.T) { client2.SendMessage(recipient, data2) // nolint var payload string - if checkReceiveClientMessage(ctx, t, client1, "room", hello2.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client1, "room", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } - if checkReceiveClientMessage(ctx, t, client2, "room", hello1.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client2, "room", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } }) @@ -2332,7 +2486,7 @@ func TestClientMessageToRoom(t *testing.T) { } func TestClientControlToRoom(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -2355,25 +2509,37 @@ func TestClientControlToRoom(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) require.NotEqual(hello1.Hello.UserId, hello2.Hello.UserId) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Give message processing some time. time.Sleep(10 * time.Millisecond) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) - recipient := api.MessageClientMessageRecipient{ + recipient := MessageClientMessageRecipient{ Type: "room", } @@ -2383,11 +2549,11 @@ func TestClientControlToRoom(t *testing.T) { client2.SendControl(recipient, data2) // nolint var payload string - if checkReceiveClientControl(ctx, t, client1, "room", hello2.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client1, "room", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } - if checkReceiveClientControl(ctx, t, client2, "room", hello1.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client2, "room", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } }) @@ -2395,7 +2561,7 @@ func TestClientControlToRoom(t *testing.T) { } func TestClientMessageToCall(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -2418,26 +2584,38 @@ func TestClientMessageToCall(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) require.NotEqual(hello1.Hello.UserId, hello2.Hello.UserId) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Give message processing some time. time.Sleep(10 * time.Millisecond) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) // Simulate request from the backend that somebody joined the call. - users := []api.StringMap{ + users := []map[string]interface{}{ { "sessionId": hello1.Hello.SessionId, "inCall": 1, @@ -2446,10 +2624,10 @@ func TestClientMessageToCall(t *testing.T) { room1 := hub1.getRoom(roomId) require.NotNil(room1, "Could not find room %s", roomId) room1.PublishUsersInCallChanged(users, users) - checkReceiveClientEvent(ctx, t, client1, "update", nil) - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", nil)) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) - recipient := api.MessageClientMessageRecipient{ + recipient := MessageClientMessageRecipient{ Type: "call", } @@ -2459,7 +2637,7 @@ func TestClientMessageToCall(t *testing.T) { client2.SendMessage(recipient, data2) // nolint var payload string - if checkReceiveClientMessage(ctx, t, client1, "call", hello2.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client1, "call", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } @@ -2467,10 +2645,14 @@ func TestClientMessageToCall(t *testing.T) { ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message", "got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } // Simulate request from the backend that somebody joined the call. - users = []api.StringMap{ + users = []map[string]interface{}{ { "sessionId": hello1.Hello.SessionId, "inCall": 1, @@ -2483,16 +2665,16 @@ func TestClientMessageToCall(t *testing.T) { room2 := hub2.getRoom(roomId) require.NotNil(room2, "Could not find room %s", roomId) room2.PublishUsersInCallChanged(users, users) - checkReceiveClientEvent(ctx, t, client1, "update", nil) - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", nil)) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) client1.SendMessage(recipient, data1) // nolint client2.SendMessage(recipient, data2) // nolint - if checkReceiveClientMessage(ctx, t, client1, "call", hello2.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client1, "call", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } - if checkReceiveClientMessage(ctx, t, client2, "call", hello1.Hello, &payload) { + if err := checkReceiveClientMessage(ctx, client2, "call", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } }) @@ -2500,7 +2682,7 @@ func TestClientMessageToCall(t *testing.T) { } func TestClientControlToCall(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -2523,26 +2705,38 @@ func TestClientControlToCall(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) require.NotEqual(hello1.Hello.UserId, hello2.Hello.UserId) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Give message processing some time. time.Sleep(10 * time.Millisecond) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) // Simulate request from the backend that somebody joined the call. - users := []api.StringMap{ + users := []map[string]interface{}{ { "sessionId": hello1.Hello.SessionId, "inCall": 1, @@ -2551,10 +2745,10 @@ func TestClientControlToCall(t *testing.T) { room1 := hub1.getRoom(roomId) require.NotNil(room1, "Could not find room %s", roomId) room1.PublishUsersInCallChanged(users, users) - checkReceiveClientEvent(ctx, t, client1, "update", nil) - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", nil)) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) - recipient := api.MessageClientMessageRecipient{ + recipient := MessageClientMessageRecipient{ Type: "call", } @@ -2564,7 +2758,7 @@ func TestClientControlToCall(t *testing.T) { client2.SendControl(recipient, data2) // nolint var payload string - if checkReceiveClientControl(ctx, t, client1, "call", hello2.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client1, "call", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } @@ -2572,10 +2766,14 @@ func TestClientControlToCall(t *testing.T) { ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message", "got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } // Simulate request from the backend that somebody joined the call. - users = []api.StringMap{ + users = []map[string]interface{}{ { "sessionId": hello1.Hello.SessionId, "inCall": 1, @@ -2588,16 +2786,16 @@ func TestClientControlToCall(t *testing.T) { room2 := hub2.getRoom(roomId) require.NotNil(room2, "Could not find room %s", roomId) room2.PublishUsersInCallChanged(users, users) - checkReceiveClientEvent(ctx, t, client1, "update", nil) - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", nil)) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) client1.SendControl(recipient, data1) // nolint client2.SendControl(recipient, data2) // nolint - if checkReceiveClientControl(ctx, t, client1, "call", hello2.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client1, "call", hello2.Hello, &payload); assert.NoError(err) { assert.Equal(data2, payload) } - if checkReceiveClientControl(ctx, t, client2, "call", hello1.Hello, &payload) { + if err := checkReceiveClientControl(ctx, client2, "call", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } }) @@ -2606,229 +2804,162 @@ func TestClientControlToCall(t *testing.T) { func TestJoinRoom(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) - assert.Nil(roomMsg.Room.Bandwidth) // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) // Leave room. - roomMsg = MustSucceed2(t, client.JoinRoom, ctx, "") - require.Empty(roomMsg.Room.RoomId) -} - -func TestJoinRoomBackendBandwidth(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTestWithConfig(t, func(server *httptest.Server) (*goconf.ConfigFile, error) { - config, err := getTestConfig(server) - if err != nil { - return nil, err - } - - config.AddOption("backend", "maxstreambitrate", "1000") - config.AddOption("backend", "maxscreenbitrate", "2000") - return config, nil - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - if bw := roomMsg.Room.Bandwidth; assert.NotNil(bw) { - assert.EqualValues(1000, bw.MaxStreamBitrate) - assert.EqualValues(2000, bw.MaxScreenBitrate) - } - - // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) -} - -func TestJoinRoomMcuBandwidth(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - mcu := sfutest.NewSFU(t) - hub.SetMcu(mcu) - - mcu.SetBandwidthLimits(1000, 2000) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - if bw := roomMsg.Room.Bandwidth; assert.NotNil(bw) { - assert.EqualValues(1000, bw.MaxStreamBitrate) - assert.EqualValues(2000, bw.MaxScreenBitrate) - } - - // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) -} - -func TestJoinRoomPreferMcuBandwidth(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTestWithConfig(t, func(server *httptest.Server) (*goconf.ConfigFile, error) { - config, err := getTestConfig(server) - if err != nil { - return nil, err - } - - config.AddOption("backend", "maxstreambitrate", "1000") - config.AddOption("backend", "maxscreenbitrate", "2000") - return config, nil - }) - - mcu := sfutest.NewSFU(t) - hub.SetMcu(mcu) - - // The MCU bandwidth limits overwrite any backend limits. - mcu.SetBandwidthLimits(100, 200) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - if bw := roomMsg.Room.Bandwidth; assert.NotNil(bw) { - assert.EqualValues(100, bw.MaxStreamBitrate) - assert.EqualValues(200, bw.MaxScreenBitrate) - } - - // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) + roomMsg, err = client.JoinRoom(ctx, "") + require.NoError(err) + require.Equal("", roomMsg.Room.RoomId) } func TestJoinInvalidRoom(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-invalid-room" - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "ABCD", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: roomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId)), + SessionId: roomId + "-" + hello.Hello.SessionId, }, } require.NoError(client.WriteJSON(msg)) - if message, ok := client.RunUntilMessage(ctx); ok { - assert.Equal(msg.Id, message.Id) - if checkMessageType(t, message, "error") { - assert.Equal("no_such_room", message.Error.Code) - } + message, err := client.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkUnexpectedClose(err)) + + assert.Equal(msg.Id, message.Id) + if assert.NoError(checkMessageType(message, "error")) { + assert.Equal("no_such_room", message.Error.Code) } } func TestJoinRoomTwice(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) require.Equal(string(testRoomProperties), string(roomMsg.Room.Properties)) // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "ABCD", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: roomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s-2", roomId, client.publicId)), + SessionId: roomId + "-" + client.publicId + "-2", }, } require.NoError(client.WriteJSON(msg)) - if message, ok := client.RunUntilMessage(ctx); ok { - assert.Equal(msg.Id, message.Id) - if checkMessageType(t, message, "error") { - assert.Equal("already_joined", message.Error.Code) - if assert.NotEmpty(message.Error.Details) { - var roomDetails api.RoomErrorDetails - if err := json.Unmarshal(message.Error.Details, &roomDetails); assert.NoError(err) { - if assert.NotNil(roomDetails.Room, "%+v", message) { - assert.Equal(roomId, roomDetails.Room.RoomId) - assert.Equal(string(testRoomProperties), string(roomDetails.Room.Properties)) - } - } - } + message, err := client.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkUnexpectedClose(err)) + + assert.Equal(msg.Id, message.Id) + if assert.NoError(checkMessageType(message, "error")) { + assert.Equal("already_joined", message.Error.Code) + assert.NotNil(message.Error.Details) + } + + var roomDetails RoomErrorDetails + if err := json.Unmarshal(message.Error.Details, &roomDetails); assert.NoError(err) { + if assert.NotNil(roomDetails.Room, "%+v", message) { + assert.Equal(roomId, roomDetails.Room.RoomId) + assert.Equal(string(testRoomProperties), string(roomDetails.Room.Properties)) } } } func TestExpectAnonymousJoinRoom(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(authAnonymousUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, authAnonymousUserId) - assert.Empty(hello.Hello.UserId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) + hello, err := client.RunUntilHello(ctx) + if assert.NoError(err) { + assert.Empty(hello.Hello.UserId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) + } // Perform housekeeping in the future, this will cause the connection to // be terminated because the anonymous client didn't join a room. - hub.performHousekeeping(time.Now().Add(anonmyousJoinRoomTimeout + time.Second)) + performHousekeeping(hub, time.Now().Add(anonmyousJoinRoomTimeout+time.Second)) - if message, ok := client.RunUntilMessage(ctx); ok { - if checkMessageType(t, message, "bye") { + if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { + if assert.NoError(checkMessageType(message, "bye")) { assert.Equal("room_join_timeout", message.Bye.Reason, "%+v", message.Bye) } } @@ -2840,46 +2971,60 @@ func TestExpectAnonymousJoinRoom(t *testing.T) { func TestExpectAnonymousJoinRoomAfterLeave(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(authAnonymousUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, authAnonymousUserId) - assert.Empty(hello.Hello.UserId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) - assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) + hello, err := client.RunUntilHello(ctx) + if assert.NoError(err) { + assert.Empty(hello.Hello.UserId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.SessionId, "%+v", hello.Hello) + assert.NotEmpty(hello.Hello.ResumeId, "%+v", hello.Hello) + } // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) // Perform housekeeping in the future, this will keep the connection as the // session joined a room. - hub.performHousekeeping(time.Now().Add(anonmyousJoinRoomTimeout + time.Second)) + performHousekeeping(hub, time.Now().Add(anonmyousJoinRoomTimeout+time.Second)) // No message about the closing is sent to the new connection. ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel2() - client.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } // Leave room - roomMsg = MustSucceed2(t, client.JoinRoom, ctx, "") - require.Empty(roomMsg.Room.RoomId) + roomMsg, err = client.JoinRoom(ctx, "") + require.NoError(err) + require.Equal("", roomMsg.Room.RoomId) // Perform housekeeping in the future, this will cause the connection to // be terminated because the anonymous client didn't join a room. - hub.performHousekeeping(time.Now().Add(anonmyousJoinRoomTimeout + time.Second)) + performHousekeeping(hub, time.Now().Add(anonmyousJoinRoomTimeout+time.Second)) - if message, ok := client.RunUntilMessage(ctx); ok { - if checkMessageType(t, message, "bye") { + if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { + if assert.NoError(checkMessageType(message, "bye")) { assert.Equal("room_join_timeout", message.Bye.Reason, "%+v", message.Bye) } } @@ -2891,107 +3036,146 @@ func TestExpectAnonymousJoinRoomAfterLeave(t *testing.T) { func TestJoinRoomChange(t *testing.T) { t.Parallel() - require := require.New(t) - hub, _, _, server := CreateHubForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) - - // Change room. - roomId = "other-test-room" - roomMsg = MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) - - // Leave room. - roomMsg = MustSucceed2(t, client.JoinRoom, ctx, "") - require.Empty(roomMsg.Room.RoomId) -} - -func TestJoinMultiple(t *testing.T) { - t.Parallel() - require := require.New(t) - hub, _, _, server := CreateHubForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) - - // Join room by id (first client). - roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event. - client1.RunUntilJoined(ctx, hello1.Hello) - - // Join room by id (second client). - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event for the first and the second client. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) - // The first client will also receive a "joined" event from the second client. - client1.RunUntilJoined(ctx, hello2.Hello) - - // Leave room. - roomMsg = MustSucceed2(t, client1.JoinRoom, ctx, "") - require.Empty(roomMsg.Room.RoomId) - - // The second client will now receive a "left" event - client2.RunUntilLeft(ctx, hello1.Hello) - - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, "") - require.Empty(roomMsg.Room.RoomId) -} - -func TestJoinDisplaynamesPermission(t *testing.T) { - t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event. + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) + + // Change room. + roomId = "other-test-room" + roomMsg, err = client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event. + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) + + // Leave room. + roomMsg, err = client.JoinRoom(ctx, "") + require.NoError(err) + require.Equal("", roomMsg.Room.RoomId) +} + +func TestJoinMultiple(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) + + // Join room by id (first client). + roomId := "test-room" + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event. + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + // Join room by id (second client). + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event for the first and the second client. + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + // The first client will also receive a "joined" event from the second client. + assert.NoError(client1.RunUntilJoined(ctx, hello2.Hello)) + + // Leave room. + roomMsg, err = client1.JoinRoom(ctx, "") + require.NoError(err) + require.Equal("", roomMsg.Room.RoomId) + + // The second client will now receive a "left" event + assert.NoError(client2.RunUntilLeft(ctx, hello1.Hello)) + + roomMsg, err = client2.JoinRoom(ctx, "") + require.NoError(err) + require.Equal("", roomMsg.Room.RoomId) +} + +func TestJoinDisplaynamesPermission(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) session2 := hub.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) // Client 2 may not receive display names. - session2.SetPermissions([]api.Permission{api.PERMISSION_HIDE_DISPLAYNAMES}) + session2.SetPermissions([]Permission{PERMISSION_HIDE_DISPLAYNAMES}) // Join room by id (first client). roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // We will receive a "joined" event. - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) // Join room by id (second client). - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // We will receive a "joined" event for the first and the second client. - if events, unexpected, ok := client2.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello); ok { + if events, unexpected, err := client2.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello); assert.NoError(err) { assert.Empty(unexpected) if assert.Len(events, 2) { assert.Nil(events[0].User) @@ -2999,7 +3183,7 @@ func TestJoinDisplaynamesPermission(t *testing.T) { } } // The first client will also receive a "joined" event from the second client. - if events, unexpected, ok := client1.RunUntilJoinedAndReturn(ctx, hello2.Hello); ok { + if events, unexpected, err := client1.RunUntilJoinedAndReturn(ctx, hello2.Hello); assert.NoError(err) { assert.Empty(unexpected) if assert.Len(events, 1) { assert.NotNil(events[0].User) @@ -3009,6 +3193,7 @@ func TestJoinDisplaynamesPermission(t *testing.T) { func TestInitialRoomPermissions(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -3016,41 +3201,55 @@ func TestInitialRoomPermissions(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId + "1")) + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room-initial-permissions" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) session := hub.GetSessionByPublicId(hello.Hello.SessionId).(*ClientSession) require.NotNil(session, "Session %s does not exist", hello.Hello.SessionId) - assert.True(session.HasPermission(api.PERMISSION_MAY_PUBLISH_AUDIO), "Session %s should have %s, got %+v", session.PublicId(), api.PERMISSION_MAY_PUBLISH_AUDIO, session.GetPermissions()) - assert.False(session.HasPermission(api.PERMISSION_MAY_PUBLISH_VIDEO), "Session %s should not have %s, got %+v", session.PublicId(), api.PERMISSION_MAY_PUBLISH_VIDEO, session.GetPermissions()) + assert.True(session.HasPermission(PERMISSION_MAY_PUBLISH_AUDIO), "Session %s should have %s, got %+v", session.PublicId(), PERMISSION_MAY_PUBLISH_AUDIO, session.permissions) + assert.False(session.HasPermission(PERMISSION_MAY_PUBLISH_VIDEO), "Session %s should not have %s, got %+v", session.PublicId(), PERMISSION_MAY_PUBLISH_VIDEO, session.permissions) } func TestJoinRoomSwitchClient(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + hello, err := client.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room-slow" - msg := &api.ClientMessage{ + msg := &ClientMessage{ Id: "ABCD", Type: "room", - Room: &api.RoomClientMessage{ + Room: &RoomClientMessage{ RoomId: roomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId)), + SessionId: roomId + "-" + hello.Hello.SessionId, }, } require.NoError(client.WriteJSON(msg)) @@ -3065,50 +3264,291 @@ func TestJoinRoomSwitchClient(t *testing.T) { client2 := NewTestClient(t, server, hub) defer client2.CloseWithBye() require.NoError(client2.SendHelloResume(hello.Hello.ResumeId)) - if hello2, ok := client2.RunUntilHello(ctx); ok { + if hello2, err := client2.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId, hello2.Hello.UserId, "%+v", hello2.Hello) assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId, "%+v", hello2.Hello) assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId, "%+v", hello2.Hello) } - roomMsg := MustSucceed1(t, client2.RunUntilMessage, ctx) - if checkMessageType(t, roomMsg, "room") { - assert.Equal(roomId, roomMsg.Room.RoomId) - } + roomMsg, err := client2.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkUnexpectedClose(err)) + require.NoError(checkMessageType(roomMsg, "room")) + require.Equal(roomId, roomMsg.Room.RoomId) // We will receive a "joined" event. - client2.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello.Hello)) // Leave room. - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, "") - require.Empty(roomMsg.Room.RoomId) + roomMsg, err = client2.JoinRoom(ctx, "") + require.NoError(err) + require.Equal("", roomMsg.Room.RoomId) +} + +func TestGetRealUserIP(t *testing.T) { + testcases := []struct { + expected string + headers http.Header + trusted string + addr string + }{ + { + "192.168.1.2", + nil, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "10.11.12.13", + nil, + "192.168.0.0/16", + "10.11.12.13:23456", + }, + { + "10.11.12.13", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "2002:db8::1", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"2002:db8::1"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "11.12.13.14", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "10.11.12.13", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"}, + }, + "2001:db8::/48", + "[2001:db8::1]:23456", + }, + { + "2002:db8::1", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"2002:db8::1"}, + }, + "2001:db8::/48", + "[2001:db8::1]:23456", + }, + { + "2002:db8::1", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 192.168.30.32"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "2002:db8::1", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 2001:db8::1"}, + }, + "192.168.0.0/16, 2001:db8::/48", + "192.168.1.2:23456", + }, + { + "2002:db8::1", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 192.168.30.32"}, + }, + "192.168.0.0/16, 2001:db8::/48", + "[2001:db8::1]:23456", + }, + { + "2002:db8::1", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 2001:db8::2"}, + }, + "2001:db8::/48", + "[2001:db8::1]:23456", + }, + // "X-Real-IP" has preference before "X-Forwarded-For" + { + "10.11.12.13", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"}, + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + // Multiple "X-Forwarded-For" headers are merged. + { + "11.12.13.14", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14", "192.168.30.32"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "11.12.13.14", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"1.2.3.4", "11.12.13.14", "192.168.30.32"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "11.12.13.14", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"1.2.3.4", "2.3.4.5", "11.12.13.14", "192.168.31.32", "192.168.30.32"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + // Headers are ignored if coming from untrusted clients. + { + "10.11.12.13", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"11.12.13.14"}, + }, + "192.168.0.0/16", + "10.11.12.13:23456", + }, + { + "10.11.12.13", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"}, + }, + "192.168.0.0/16", + "10.11.12.13:23456", + }, + // X-Forwarded-For is filtered for trusted proxies. + { + "1.2.3.4", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "1.2.3.4", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4, 192.168.2.3"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "10.11.12.13", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4"}, + }, + "192.168.0.0/16", + "10.11.12.13:23456", + }, + // Invalid IPs are ignored. + { + "192.168.1.2", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "11.12.13.14", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"}, + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "11.12.13.14", + http.Header{ + http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"}, + http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32, proxy1"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "192.168.1.2", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"this-is-not-an-ip"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + { + "192.168.2.3", + http.Header{ + http.CanonicalHeaderKey("x-forwarded-for"): []string{"this-is-not-an-ip, 192.168.2.3"}, + }, + "192.168.0.0/16", + "192.168.1.2:23456", + }, + } + + for _, tc := range testcases { + trustedProxies, err := ParseAllowedIps(tc.trusted) + if !assert.NoError(t, err, "invalid trusted proxies in %+v", tc) { + continue + } + request := &http.Request{ + RemoteAddr: tc.addr, + Header: tc.headers, + } + assert.Equal(t, tc.expected, GetRealUserIP(request, trustedProxies), "failed for %+v", tc) + } } func TestClientMessageToSessionIdWhileDisconnected(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) client2.Close() assert.NoError(client2.WaitForClientRemoved(ctx)) - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, } - chat_refresh := "{\"type\":\"foo\",\"foo\":{\"testing\":true}}" - var data1 api.StringMap + // The two chat messages should get combined into one when receiving pending messages. + chat_refresh := "{\"type\":\"chat\",\"chat\":{\"refresh\":true}}" + var data1 map[string]interface{} require.NoError(json.Unmarshal([]byte(chat_refresh), &data1)) client1.SendMessage(recipient2, data1) // nolint + client1.SendMessage(recipient2, data1) // nolint // Simulate some time until client resumes the session. time.Sleep(10 * time.Millisecond) @@ -3116,120 +3556,68 @@ func TestClientMessageToSessionIdWhileDisconnected(t *testing.T) { client2 = NewTestClient(t, server, hub) defer client2.CloseWithBye() require.NoError(client2.SendHelloResume(hello2.Hello.ResumeId)) - if hello3, ok := client2.RunUntilHello(ctx); ok { + if hello3, err := client2.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId+"2", hello3.Hello.UserId, "%+v", hello3.Hello) assert.Equal(hello2.Hello.SessionId, hello3.Hello.SessionId, "%+v", hello3.Hello) assert.Equal(hello2.Hello.ResumeId, hello3.Hello.ResumeId, "%+v", hello3.Hello) } - var payload api.StringMap - if checkReceiveClientMessage(ctx, t, client2, "session", hello1.Hello, &payload) { + var payload map[string]interface{} + if err := checkReceiveClientMessage(ctx, client2, "session", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } -} - -func TestCombineChatRefreshWhileDisconnected(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client.RunUntilJoined(ctx, hello.Hello) - - room := hub.getRoom(roomId) - require.NotNil(room) - - client.Close() - assert.NoError(client.WaitForClientRemoved(ctx)) - - // The two chat messages should get combined into one when receiving pending messages. - chat_refresh := "{\"type\":\"chat\",\"chat\":{\"refresh\":true}}" - var data api.StringMap - require.NoError(json.Unmarshal([]byte(chat_refresh), &data)) - - // Simulate requests from the backend. - room.processAsyncMessage(&events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "message", - Message: &talk.BackendRoomMessageRequest{ - Data: json.RawMessage(chat_refresh), - }, - }, - }) - room.processAsyncMessage(&events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "message", - Message: &talk.BackendRoomMessageRequest{ - Data: json.RawMessage(chat_refresh), - }, - }, - }) - - // Simulate some time until client resumes the session. - time.Sleep(10 * time.Millisecond) - - client = NewTestClient(t, server, hub) - defer client.CloseWithBye() - require.NoError(client.SendHelloResume(hello.Hello.ResumeId)) - if hello2, ok := client.RunUntilHello(ctx); ok { - assert.Equal(hello.Hello.UserId, hello2.Hello.UserId) - assert.Equal(hello.Hello.SessionId, hello2.Hello.SessionId) - assert.Equal(hello.Hello.ResumeId, hello2.Hello.ResumeId) - } - - if msg, ok := client.RunUntilRoomMessage(ctx); ok { - assert.Equal(roomId, msg.RoomId) - var payload api.StringMap - if err := json.Unmarshal(msg.Data, &payload); assert.NoError(err) { - assert.Equal(data, payload) - } - } ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel2() - client.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if err := checkReceiveClientMessage(ctx2, client2, "session", hello1.Hello, &payload); err == nil { + assert.Fail("Expected no payload, got %+v", payload) + } else { + assert.ErrorIs(err, ErrNoMessageReceived) + } } func TestRoomParticipantsListUpdateWhileDisconnected(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + require.NotEqual(hello1.Hello.SessionId, hello2.Hello.SessionId) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Give message processing some time. time.Sleep(10 * time.Millisecond) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) // Simulate request from the backend that somebody joined the call. - users := []api.StringMap{ + users := []map[string]interface{}{ { "sessionId": "the-session-id", "inCall": 1, @@ -3238,7 +3626,7 @@ func TestRoomParticipantsListUpdateWhileDisconnected(t *testing.T) { room := hub.getRoom(roomId) require.NotNil(room, "Could not find room %s", roomId) room.PublishUsersInCallChanged(users, users) - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) client2.Close() assert.NoError(client2.WaitForClientRemoved(ctx)) @@ -3248,20 +3636,20 @@ func TestRoomParticipantsListUpdateWhileDisconnected(t *testing.T) { // Give asynchronous events some time to be processed. time.Sleep(100 * time.Millisecond) - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, } chat_refresh := "{\"type\":\"chat\",\"chat\":{\"refresh\":true}}" - var data1 api.StringMap + var data1 map[string]interface{} require.NoError(json.Unmarshal([]byte(chat_refresh), &data1)) client1.SendMessage(recipient2, data1) // nolint client2 = NewTestClient(t, server, hub) defer client2.CloseWithBye() require.NoError(client2.SendHelloResume(hello2.Hello.ResumeId)) - if hello3, ok := client2.RunUntilHello(ctx); ok { + if hello3, err := client2.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId+"2", hello3.Hello.UserId, "%+v", hello3.Hello) assert.Equal(hello2.Hello.SessionId, hello3.Hello.SessionId, "%+v", hello3.Hello) assert.Equal(hello2.Hello.ResumeId, hello3.Hello.ResumeId, "%+v", hello3.Hello) @@ -3269,21 +3657,25 @@ func TestRoomParticipantsListUpdateWhileDisconnected(t *testing.T) { // The participants list update event is triggered again after the session resume. // TODO(jojo): Check contents of message and try with multiple users. - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) - var payload api.StringMap - if checkReceiveClientMessage(ctx, t, client2, "session", hello1.Hello, &payload) { + var payload map[string]interface{} + if err := checkReceiveClientMessage(ctx, client2, "session", hello1.Hello, &payload); assert.NoError(err) { assert.Equal(data1, payload) } ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if err := checkReceiveClientMessage(ctx2, client2, "session", hello1.Hello, &payload); err == nil { + assert.Fail("Expected no payload, got %+v", payload) + } else { + assert.ErrorIs(err, ErrNoMessageReceived) + } } func TestClientTakeoverRoomSession(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -3308,15 +3700,22 @@ func RunTestClientTakeoverRoomSession(t *testing.T) { hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) } + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room-takeover-room-session" - roomSessionid := api.RoomSessionId("room-session-id") - roomMsg := MustSucceed3(t, client1.JoinRoomWithRoomSession, ctx, roomId, roomSessionid) + roomSessionid := "room-session-id" + roomMsg, err := client1.JoinRoomWithRoomSession(ctx, roomId, roomSessionid) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) hubRoom := hub1.getRoom(roomId) @@ -3325,28 +3724,46 @@ func RunTestClientTakeoverRoomSession(t *testing.T) { session1 := hub1.GetSessionByPublicId(hello1.Hello.SessionId) require.NotNil(session1, "There should be a session %s", hello1.Hello.SessionId) - client3, hello3 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"3") + client3 := NewTestClient(t, server2, hub2) + defer client3.CloseWithBye() - roomMsg = MustSucceed3(t, client3.JoinRoomWithRoomSession, ctx, roomId, roomSessionid+"other") + require.NoError(client3.SendHello(testDefaultUserId + "3")) + + hello3, err := client3.RunUntilHello(ctx) + require.NoError(err) + + roomMsg, err = client3.JoinRoomWithRoomSession(ctx, roomId, roomSessionid+"other") + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Wait until both users have joined. WaitForUsersJoined(ctx, t, client1, hello1, client3, hello3) - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() - roomMsg = MustSucceed3(t, client2.JoinRoomWithRoomSession, ctx, roomId, roomSessionid) + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + roomMsg, err = client2.JoinRoomWithRoomSession(ctx, roomId, roomSessionid) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // The first client got disconnected with a reason in a "Bye" message. - if msg, ok := client1.RunUntilMessage(ctx); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { assert.Equal("bye", msg.Type, "%+v", msg) if assert.NotNil(msg.Bye, "%+v", msg) { assert.Equal("room_session_reconnected", msg.Bye.Reason, "%+v", msg) } } - client1.RunUntilClosed(ctx) + if msg, err := client1.RunUntilMessage(ctx); err == nil { + assert.Fail("Expected error but received %+v", msg) + } else if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + assert.Fail("Expected close error but received %+v", err) + } // The first session has been closed session1 = hub1.GetSessionByPublicId(hello1.Hello.SessionId) @@ -3354,59 +3771,69 @@ func RunTestClientTakeoverRoomSession(t *testing.T) { // The new client will receive "joined" events for the existing client3 and // himself. - client2.RunUntilJoined(ctx, hello3.Hello, hello2.Hello) + assert.NoError(client2.RunUntilJoined(ctx, hello3.Hello, hello2.Hello)) // No message about the closing is sent to the new connection. ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } // The permanently connected client will receive a "left" event from the - // overridden session and a "joined" for the new session. - msg1 := MustSucceed1(t, client3.RunUntilMessage, ctx) - msg2 := MustSucceed1(t, client3.RunUntilMessage, ctx) - if msg1.Type == "event" && msg2.Type == "event" && msg1.Event.Type == "join" { - t.Logf("Switching messages order") - msg1, msg2 = msg2, msg1 - } - - client3.checkMessageRoomLeave(msg1, hello1.Hello) - if checkMessageType(t, msg2, "event") && - assert.Equal("room", msg2.Event.Target, "invalid target in %+v", msg2) && - assert.Equal("join", msg2.Event.Type, "invalid event type in %+v", msg2) && - assert.Len(msg2.Event.Join, 1, "invalid number of join event entries: %+v", msg2.Event) { - assert.Equal(hello2.Hello.SessionId, msg2.Event.Join[0].SessionId) - assert.Equal(hello2.Hello.UserId, msg2.Event.Join[0].UserId) - } + // overridden session and a "joined" for the new session. In that order as + // both were on the same server. + assert.NoError(client3.RunUntilLeft(ctx, hello1.Hello)) + assert.NoError(client3.RunUntilJoined(ctx, hello2.Hello)) } func TestClientSendOfferPermissions(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) + assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - mcu := sfutest.NewSFU(t) + mcu, err := NewTestMCU() + require.NoError(err) require.NoError(mcu.Start(ctx)) defer mcu.Stop() hub.SetMcu(mcu) - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // Give message processing some time. time.Sleep(10 * time.Millisecond) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) @@ -3417,42 +3844,43 @@ func TestClientSendOfferPermissions(t *testing.T) { require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) // Client 1 is the moderator - session1.SetPermissions([]api.Permission{api.PERMISSION_MAY_PUBLISH_MEDIA, api.PERMISSION_MAY_PUBLISH_SCREEN}) + session1.SetPermissions([]Permission{PERMISSION_MAY_PUBLISH_MEDIA, PERMISSION_MAY_PUBLISH_SCREEN}) // Client 2 is a guest participant. - session2.SetPermissions([]api.Permission{}) + session2.SetPermissions([]Permission{}) // Client 2 may not send an offer (he doesn't have the necessary permissions). - require.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "sendoffer", Sid: "12345", RoomType: "screen", })) - msg := MustSucceed1(t, client2.RunUntilMessage, ctx) - require.True(checkMessageError(t, msg, "not_allowed")) + msg, err := client2.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkMessageError(msg, "not_allowed")) - require.NoError(client1.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "offer", Sid: "12345", RoomType: "screen", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, }, })) - client1.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo) + require.NoError(client1.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo)) // Client 1 may send an offer. - require.NoError(client1.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "sendoffer", Sid: "54321", RoomType: "screen", @@ -3462,75 +3890,19 @@ func TestClientSendOfferPermissions(t *testing.T) { ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel2() - client1.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client1.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } // ...but the other peer will get an offer. - client2.RunUntilOffer(ctx, mock.MockSdpOfferAudioAndVideo) + require.NoError(client2.RunUntilOffer(ctx, MockSdpOfferAudioAndVideo)) } func TestClientSendOfferPermissionsAudioOnly(t *testing.T) { t.Parallel() - require := require.New(t) - hub, _, _, server := CreateHubForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - mcu := sfutest.NewSFU(t) - require.NoError(mcu.Start(ctx)) - defer mcu.Stop() - - hub.SetMcu(mcu) - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client.RunUntilJoined(ctx, hello.Hello) - - session := hub.GetSessionByPublicId(hello.Hello.SessionId).(*ClientSession) - require.NotNil(session, "Session %s does not exist", hello.Hello.SessionId) - - // Client is allowed to send audio only. - session.SetPermissions([]api.Permission{api.PERMISSION_MAY_PUBLISH_AUDIO}) - - // Client may not send an offer with audio and video. - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "54321", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - })) - - msg := MustSucceed1(t, client.RunUntilMessage, ctx) - require.True(checkMessageError(t, msg, "not_allowed")) - - // Client may send an offer (audio only). - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "54321", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioOnly, - }, - })) - - client.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioOnly) -} - -func TestClientSendOfferPermissionsAudioVideo(t *testing.T) { - t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -3538,55 +3910,135 @@ func TestClientSendOfferPermissionsAudioVideo(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - mcu := sfutest.NewSFU(t) + mcu, err := NewTestMCU() + require.NoError(err) require.NoError(mcu.Start(ctx)) defer mcu.Stop() hub.SetMcu(mcu) - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) - session := hub.GetSessionByPublicId(hello.Hello.SessionId).(*ClientSession) - require.NotNil(session, "Session %s does not exist", hello.Hello.SessionId) + session1 := hub.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession) + require.NotNil(session1, "Session %s does not exist", hello1.Hello.SessionId) - // Client is allowed to send audio and video. - session.SetPermissions([]api.Permission{api.PERMISSION_MAY_PUBLISH_AUDIO, api.PERMISSION_MAY_PUBLISH_VIDEO}) + // Client is allowed to send audio only. + session1.SetPermissions([]Permission{PERMISSION_MAY_PUBLISH_AUDIO}) - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ + // Client may not send an offer with audio and video. + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ + SessionId: hello1.Hello.SessionId, + }, MessageClientMessageData{ Type: "offer", Sid: "54321", RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, }, })) - require.True(client.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo)) + msg, err := client1.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkMessageError(msg, "not_allowed")) + + // Client may send an offer (audio only). + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello1.Hello.SessionId, + }, MessageClientMessageData{ + Type: "offer", + Sid: "54321", + RoomType: "video", + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioOnly, + }, + })) + + require.NoError(client1.RunUntilAnswer(ctx, MockSdpAnswerAudioOnly)) +} + +func TestClientSendOfferPermissionsAudioVideo(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mcu, err := NewTestMCU() + require.NoError(err) + require.NoError(mcu.Start(ctx)) + defer mcu.Stop() + + hub.SetMcu(mcu) + + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + session1 := hub.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession) + require.NotNil(session1, "Session %s does not exist", hello1.Hello.SessionId) + + // Client is allowed to send audio and video. + session1.SetPermissions([]Permission{PERMISSION_MAY_PUBLISH_AUDIO, PERMISSION_MAY_PUBLISH_VIDEO}) + + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello1.Hello.SessionId, + }, MessageClientMessageData{ + Type: "offer", + Sid: "54321", + RoomType: "video", + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, + }, + })) + + require.NoError(client1.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo)) // Client is no longer allowed to send video, this will stop the publisher. - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "participants", - Participants: &talk.BackendRoomParticipantsRequest{ - Changed: []api.StringMap{ + Participants: &BackendRoomParticipantsRequest{ + Changed: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId), - "permissions": []api.Permission{api.PERMISSION_MAY_PUBLISH_AUDIO}, + "sessionId": roomId + "-" + hello1.Hello.SessionId, + "permissions": []Permission{PERMISSION_MAY_PUBLISH_AUDIO}, }, }, - Users: []api.StringMap{ + Users: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId), - "permissions": []api.Permission{api.PERMISSION_MAY_PUBLISH_AUDIO}, + "sessionId": roomId + "-" + hello1.Hello.SessionId, + "permissions": []Permission{PERMISSION_MAY_PUBLISH_AUDIO}, }, }, }, @@ -3615,7 +4067,7 @@ loop: } for _, pub := range pubs { - if pub.IsClosed() { + if pub.isClosed() { break loop } } @@ -3627,6 +4079,7 @@ loop: func TestClientSendOfferPermissionsAudioVideoMedia(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) @@ -3634,56 +4087,64 @@ func TestClientSendOfferPermissionsAudioVideoMedia(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - mcu := sfutest.NewSFU(t) + mcu, err := NewTestMCU() + require.NoError(err) require.NoError(mcu.Start(ctx)) defer mcu.Stop() hub.SetMcu(mcu) - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) - client.RunUntilJoined(ctx, hello.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) - session := hub.GetSessionByPublicId(hello.Hello.SessionId).(*ClientSession) - require.NotNil(session, "Session %s does not exist", hello.Hello.SessionId) + session1 := hub.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession) + require.NotNil(session1, "Session %s does not exist", hello1.Hello.SessionId) // Client is allowed to send audio and video. - session.SetPermissions([]api.Permission{api.PERMISSION_MAY_PUBLISH_MEDIA}) + session1.SetPermissions([]Permission{PERMISSION_MAY_PUBLISH_MEDIA}) // Client may send an offer (audio and video). - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ + SessionId: hello1.Hello.SessionId, + }, MessageClientMessageData{ Type: "offer", Sid: "54321", RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, }, })) - require.True(client.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo)) + require.NoError(client1.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo)) // Client is no longer allowed to send video, this will stop the publisher. - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "participants", - Participants: &talk.BackendRoomParticipantsRequest{ - Changed: []api.StringMap{ + Participants: &BackendRoomParticipantsRequest{ + Changed: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId), - "permissions": []api.Permission{api.PERMISSION_MAY_PUBLISH_MEDIA, api.PERMISSION_MAY_CONTROL}, + "sessionId": roomId + "-" + hello1.Hello.SessionId, + "permissions": []Permission{PERMISSION_MAY_PUBLISH_MEDIA, PERMISSION_MAY_CONTROL}, }, }, - Users: []api.StringMap{ + Users: []map[string]interface{}{ { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId), - "permissions": []api.Permission{api.PERMISSION_MAY_PUBLISH_MEDIA, api.PERMISSION_MAY_CONTROL}, + "sessionId": roomId + "-" + hello1.Hello.SessionId, + "permissions": []Permission{PERMISSION_MAY_PUBLISH_MEDIA, PERMISSION_MAY_CONTROL}, }, }, }, @@ -3714,7 +4175,7 @@ loop: } for _, pub := range pubs { - if !assert.False(pub.IsClosed(), "publisher was closed") { + if !assert.False(pub.isClosed(), "publisher was closed") { break loop } } @@ -3725,11 +4186,12 @@ loop: } func TestClientRequestOfferNotInRoom(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() require := require.New(t) + assert := assert.New(t) var hub1 *Hub var hub2 *Hub var server1 *httptest.Server @@ -3746,73 +4208,91 @@ func TestClientRequestOfferNotInRoom(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - mcu := sfutest.NewSFU(t) + mcu, err := NewTestMCU() + require.NoError(err) require.NoError(mcu.Start(ctx)) defer mcu.Stop() hub1.SetMcu(mcu) hub2.SetMcu(mcu) - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed3(t, client1.JoinRoomWithRoomSession, ctx, roomId, "roomsession1") + roomMsg, err := client1.JoinRoomWithRoomSession(ctx, roomId, "roomsession1") + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // We will receive a "joined" event. - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) - require.NoError(client1.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "offer", Sid: "54321", RoomType: "screen", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, }, })) - require.True(client1.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo)) + require.NoError(client1.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo)) // Client 2 may not request an offer (he is not in the room yet). - require.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "requestoffer", Sid: "12345", RoomType: "screen", })) - msg := MustSucceed1(t, client2.RunUntilMessage, ctx) - require.True(checkMessageError(t, msg, "not_allowed")) + msg, err := client2.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkMessageError(msg, "not_allowed")) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) // We will receive a "joined" event. - require.True(client1.RunUntilJoined(ctx, hello2.Hello)) - require.True(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + require.NoError(client1.RunUntilJoined(ctx, hello2.Hello)) + require.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) // Client 2 may not request an offer (he is not in the call yet). - require.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "requestoffer", Sid: "12345", RoomType: "screen", })) - msg = MustSucceed1(t, client2.RunUntilMessage, ctx) - require.True(checkMessageError(t, msg, "not_allowed")) + msg, err = client2.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkMessageError(msg, "not_allowed")) // Simulate request from the backend that somebody joined the call. - users1 := []api.StringMap{ + users1 := []map[string]interface{}{ { "sessionId": hello2.Hello.SessionId, "inCall": 1, @@ -3821,24 +4301,25 @@ func TestClientRequestOfferNotInRoom(t *testing.T) { room2 := hub2.getRoom(roomId) require.NotNil(room2, "Could not find room %s", roomId) room2.PublishUsersInCallChanged(users1, users1) - checkReceiveClientEvent(ctx, t, client1, "update", nil) - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", nil)) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) // Client 2 may not request an offer (recipient is not in the call yet). - require.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "requestoffer", Sid: "12345", RoomType: "screen", })) - msg = MustSucceed1(t, client2.RunUntilMessage, ctx) - require.True(checkMessageError(t, msg, "not_allowed")) + msg, err = client2.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkMessageError(msg, "not_allowed")) // Simulate request from the backend that somebody joined the call. - users2 := []api.StringMap{ + users2 := []map[string]interface{}{ { "sessionId": hello1.Hello.SessionId, "inCall": 1, @@ -3847,30 +4328,30 @@ func TestClientRequestOfferNotInRoom(t *testing.T) { room1 := hub1.getRoom(roomId) require.NotNil(room1, "Could not find room %s", roomId) room1.PublishUsersInCallChanged(users2, users2) - checkReceiveClientEvent(ctx, t, client1, "update", nil) - checkReceiveClientEvent(ctx, t, client2, "update", nil) + assert.NoError(checkReceiveClientEvent(ctx, client1, "update", nil)) + assert.NoError(checkReceiveClientEvent(ctx, client2, "update", nil)) // Client 2 may request an offer now (both are in the same room and call). - require.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "requestoffer", Sid: "12345", RoomType: "screen", })) - require.True(client2.RunUntilOffer(ctx, mock.MockSdpOfferAudioAndVideo)) + require.NoError(client2.RunUntilOffer(ctx, MockSdpOfferAudioAndVideo)) - require.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ + require.NoError(client2.SendMessage(MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ + }, MessageClientMessageData{ Type: "answer", Sid: "12345", RoomType: "screen", - Payload: api.StringMap{ - "sdp": mock.MockSdpAnswerAudioAndVideo, + Payload: map[string]interface{}{ + "sdp": MockSdpAnswerAudioAndVideo, }, })) @@ -3878,14 +4359,20 @@ func TestClientRequestOfferNotInRoom(t *testing.T) { ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } }) } } func TestNoSendBetweenSessionsOnDifferentBackends(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) + assert := assert.New(t) // Clients can't send messages to sessions connected from other backends. hub, _, _, server := CreateHubWithMultipleBackendsForTest(t) @@ -3898,8 +4385,9 @@ func TestNoSendBetweenSessionsOnDifferentBackends(t *testing.T) { params1 := TestBackendClientAuthParams{ UserId: "user1", } - require.NoError(client1.SendHelloParams(server.URL+"/one", api.HelloVersionV1, "client", nil, params1)) - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) + require.NoError(client1.SendHelloParams(server.URL+"/one", HelloVersionV1, "client", nil, params1)) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) client2 := NewTestClient(t, server, hub) defer client2.CloseWithBye() @@ -3907,66 +4395,15 @@ func TestNoSendBetweenSessionsOnDifferentBackends(t *testing.T) { params2 := TestBackendClientAuthParams{ UserId: "user2", } - require.NoError(client2.SendHelloParams(server.URL+"/two", api.HelloVersionV1, "client", nil, params2)) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + require.NoError(client2.SendHelloParams(server.URL+"/two", HelloVersionV1, "client", nil, params2)) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) - recipient1 := api.MessageClientMessageRecipient{ + recipient1 := MessageClientMessageRecipient{ Type: "session", SessionId: hello1.Hello.SessionId, } - recipient2 := api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello2.Hello.SessionId, - } - - data1 := "from-1-to-2" - client1.SendMessage(recipient2, data1) // nolint - data2 := "from-2-to-1" - client2.SendMessage(recipient1, data2) // nolint - - ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel2() - - client1.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) - - ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel3() - - client2.RunUntilErrorIs(ctx3, ErrNoMessageReceived, context.DeadlineExceeded) -} - -func TestSendBetweenDifferentUrls(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubWithMultipleUrlsForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client1 := NewTestClient(t, server, hub) - defer client1.CloseWithBye() - - params1 := TestBackendClientAuthParams{ - UserId: "user1", - } - require.NoError(client1.SendHelloParams(server.URL+"/one", api.HelloVersionV1, "client", nil, params1)) - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - - client2 := NewTestClient(t, server, hub) - defer client2.CloseWithBye() - - params2 := TestBackendClientAuthParams{ - UserId: "user2", - } - require.NoError(client2.SendHelloParams(server.URL+"/two", api.HelloVersionV1, "client", nil, params2)) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) - - recipient1 := api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello1.Hello.SessionId, - } - recipient2 := api.MessageClientMessageRecipient{ + recipient2 := MessageClientMessageRecipient{ Type: "session", SessionId: hello2.Hello.SessionId, } @@ -3977,16 +4414,26 @@ func TestSendBetweenDifferentUrls(t *testing.T) { client2.SendMessage(recipient1, data2) // nolint var payload string - if checkReceiveClientMessage(ctx, t, client1, "session", hello2.Hello, &payload) { - assert.Equal(data2, payload) + ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel2() + if err := checkReceiveClientMessage(ctx2, client1, "session", hello2.Hello, &payload); err == nil { + assert.Fail("Expected no payload, got %+v", payload) + } else { + assert.ErrorIs(err, ErrNoMessageReceived) } - if checkReceiveClientMessage(ctx, t, client2, "session", hello1.Hello, &payload) { - assert.Equal(data1, payload) + + ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel3() + if err := checkReceiveClientMessage(ctx3, client2, "session", hello1.Hello, &payload); err == nil { + assert.Fail("Expected no payload, got %+v", payload) + } else { + assert.ErrorIs(err, ErrNoMessageReceived) } } func TestNoSameRoomOnDifferentBackends(t *testing.T) { t.Parallel() + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubWithMultipleBackendsForTest(t) @@ -4000,8 +4447,9 @@ func TestNoSameRoomOnDifferentBackends(t *testing.T) { params1 := TestBackendClientAuthParams{ UserId: "user1", } - require.NoError(client1.SendHelloParams(server.URL+"/one", api.HelloVersionV1, "client", nil, params1)) - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) + require.NoError(client1.SendHelloParams(server.URL+"/one", HelloVersionV1, "client", nil, params1)) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) client2 := NewTestClient(t, server, hub) defer client2.CloseWithBye() @@ -4009,21 +4457,24 @@ func TestNoSameRoomOnDifferentBackends(t *testing.T) { params2 := TestBackendClientAuthParams{ UserId: "user2", } - require.NoError(client2.SendHelloParams(server.URL+"/two", api.HelloVersionV1, "client", nil, params2)) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + require.NoError(client2.SendHelloParams(server.URL+"/two", HelloVersionV1, "client", nil, params2)) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) // Join room by id. roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) - if msg1, ok := client1.RunUntilMessage(ctx); ok { - client1.checkMessageJoined(msg1, hello1.Hello) + if msg1, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkMessageJoined(msg1, hello1.Hello)) } - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) require.Equal(roomId, roomMsg.Room.RoomId) - if msg2, ok := client2.RunUntilMessage(ctx); ok { - client2.checkMessageJoined(msg2, hello2.Hello) + if msg2, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client2.checkMessageJoined(msg2, hello2.Hello)) } hub.ru.RLock() @@ -4035,77 +4486,12 @@ func TestNoSameRoomOnDifferentBackends(t *testing.T) { hub.ru.RUnlock() if assert.Len(rooms, 2) { - assert.False(rooms[0].IsEqual(rooms[1]), "Rooms should be different: %+v", rooms) + if rooms[0].IsEqual(rooms[1]) { + assert.Fail("Rooms should be different: %+v", rooms) + } } - recipient := api.MessageClientMessageRecipient{ - Type: "room", - } - - data1 := "from-1-to-2" - client1.SendMessage(recipient, data1) // nolint - data2 := "from-2-to-1" - client2.SendMessage(recipient, data2) // nolint - - ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel2() - - client1.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) - - ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel3() - - client2.RunUntilErrorIs(ctx3, ErrNoMessageReceived, context.DeadlineExceeded) -} - -func TestSameRoomOnDifferentUrls(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubWithMultipleUrlsForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client1 := NewTestClient(t, server, hub) - defer client1.CloseWithBye() - - params1 := TestBackendClientAuthParams{ - UserId: "user1", - } - require.NoError(client1.SendHelloParams(server.URL+"/one", api.HelloVersionV1, "client", nil, params1)) - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - - client2 := NewTestClient(t, server, hub) - defer client2.CloseWithBye() - - params2 := TestBackendClientAuthParams{ - UserId: "user2", - } - require.NoError(client2.SendHelloParams(server.URL+"/two", api.HelloVersionV1, "client", nil, params2)) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) - - hub.ru.RLock() - var rooms []*Room - for _, room := range hub.rooms { - defer room.Close() - rooms = append(rooms, room) - } - hub.ru.RUnlock() - - assert.Len(rooms, 1) - - recipient := api.MessageClientMessageRecipient{ + recipient := MessageClientMessageRecipient{ Type: "room", } @@ -4115,159 +4501,25 @@ func TestSameRoomOnDifferentUrls(t *testing.T) { client2.SendMessage(recipient, data2) // nolint var payload string - if checkReceiveClientMessage(ctx, t, client1, "room", hello2.Hello, &payload) { - assert.Equal(data2, payload) + ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel2() + if err := checkReceiveClientMessage(ctx2, client1, "session", hello2.Hello, &payload); err == nil { + assert.Fail("Expected no payload, got %+v", payload) + } else { + assert.ErrorIs(err, ErrNoMessageReceived) } - if checkReceiveClientMessage(ctx, t, client2, "room", hello1.Hello, &payload) { - assert.Equal(data1, payload) + + ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel3() + if err := checkReceiveClientMessage(ctx3, client2, "session", hello1.Hello, &payload); err == nil { + assert.Fail("Expected no payload, got %+v", payload) + } else { + assert.ErrorIs(err, ErrNoMessageReceived) } } func TestClientSendOffer(t *testing.T) { - t.Parallel() - for _, subtest := range clusteredTests { - t.Run(subtest, func(t *testing.T) { - t.Parallel() - require := require.New(t) - var hub1 *Hub - var hub2 *Hub - var server1 *httptest.Server - var server2 *httptest.Server - if isLocalTest(t) { - hub1, _, _, server1 = CreateHubForTest(t) - - hub2 = hub1 - server2 = server1 - } else { - hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) - } - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - mcu := sfutest.NewSFU(t) - require.NoError(mcu.Start(ctx)) - defer mcu.Stop() - - hub1.SetMcu(mcu) - hub2.SetMcu(mcu) - - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed3(t, client1.JoinRoomWithRoomSession, ctx, roomId, "roomsession1") - require.Equal(roomId, roomMsg.Room.RoomId) - - // Give message processing some time. - time.Sleep(10 * time.Millisecond) - - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) - - require.NoError(client1.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "12345", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - })) - - require.True(client1.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo)) - - require.NoError(client1.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello2.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "sendoffer", - RoomType: "video", - })) - - // The sender won't get a reply... - ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond) - defer cancel2() - - client1.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) - - // ...but the other peer will get an offer. - client2.RunUntilOffer(ctx, mock.MockSdpOfferAudioAndVideo) - }) - } -} - -func TestClientUnshareScreen(t *testing.T) { - t.Parallel() - require := require.New(t) - hub, _, _, server := CreateHubForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - mcu := sfutest.NewSFU(t) - require.NoError(mcu.Start(ctx)) - defer mcu.Stop() - - hub.SetMcu(mcu) - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client.RunUntilJoined(ctx, hello.Hello) - - session := hub.GetSessionByPublicId(hello.Hello.SessionId).(*ClientSession) - require.NotNil(session, "Session %s does not exist", hello.Hello.SessionId) - - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "54321", - RoomType: "screen", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioOnly, - }, - })) - - client.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioOnly) - - publisher := mcu.GetPublisher(hello.Hello.SessionId) - require.NotNil(publisher, "No publisher for %s found", hello.Hello.SessionId) - require.False(publisher.IsClosed(), "Publisher %s should not be closed", hello.Hello.SessionId) - - old := cleanupScreenPublisherDelay - cleanupScreenPublisherDelay = time.Millisecond - defer func() { - cleanupScreenPublisherDelay = old - }() - - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "unshareScreen", - Sid: "54321", - RoomType: "screen", - })) - - time.Sleep(10 * time.Millisecond) - - require.True(publisher.IsClosed(), "Publisher %s should be closed", hello.Hello.SessionId) -} - -func TestVirtualClientSessions(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -4289,59 +4541,244 @@ func TestVirtualClientSessions(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId) - roomId := "test-room" - MustSucceed2(t, client1.JoinRoom, ctx, roomId) + mcu, err := NewTestMCU() + require.NoError(err) + require.NoError(mcu.Start(ctx)) + defer mcu.Stop() - client1.RunUntilJoined(ctx, hello1.Hello) + hub1.SetMcu(mcu) + hub2.SetMcu(mcu) + + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client1.JoinRoomWithRoomSession(ctx, roomId, "roomsession1") + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // Give message processing some time. + time.Sleep(10 * time.Millisecond) + + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) + + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello1.Hello.SessionId, + }, MessageClientMessageData{ + Type: "offer", + Sid: "12345", + RoomType: "video", + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioAndVideo, + }, + })) + + require.NoError(client1.RunUntilAnswer(ctx, MockSdpAnswerAudioAndVideo)) + + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello2.Hello.SessionId, + }, MessageClientMessageData{ + Type: "sendoffer", + RoomType: "video", + })) + + // The sender won't get a reply... + ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel2() + + if message, err := client1.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } + + // ...but the other peer will get an offer. + require.NoError(client2.RunUntilOffer(ctx, MockSdpOfferAudioAndVideo)) + }) + } +} + +func TestClientUnshareScreen(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + mcu, err := NewTestMCU() + require.NoError(err) + require.NoError(mcu.Start(ctx)) + defer mcu.Stop() + + hub.SetMcu(mcu) + + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + session1 := hub.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession) + require.NotNil(session1, "Session %s does not exist", hello1.Hello.SessionId) + + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello1.Hello.SessionId, + }, MessageClientMessageData{ + Type: "offer", + Sid: "54321", + RoomType: "screen", + Payload: map[string]interface{}{ + "sdp": MockSdpOfferAudioOnly, + }, + })) + + require.NoError(client1.RunUntilAnswer(ctx, MockSdpAnswerAudioOnly)) + + publisher := mcu.GetPublisher(hello1.Hello.SessionId) + require.NotNil(publisher, "No publisher for %s found", hello1.Hello.SessionId) + require.False(publisher.isClosed(), "Publisher %s should not be closed", hello1.Hello.SessionId) + + old := cleanupScreenPublisherDelay + cleanupScreenPublisherDelay = time.Millisecond + defer func() { + cleanupScreenPublisherDelay = old + }() + + require.NoError(client1.SendMessage(MessageClientMessageRecipient{ + Type: "session", + SessionId: hello1.Hello.SessionId, + }, MessageClientMessageData{ + Type: "unshareScreen", + Sid: "54321", + RoomType: "screen", + })) + + time.Sleep(10 * time.Millisecond) + + require.True(publisher.isClosed(), "Publisher %s should be closed", hello1.Hello.SessionId) +} + +func TestVirtualClientSessions(t *testing.T) { + CatchLogForTest(t) + for _, subtest := range clusteredTests { + t.Run(subtest, func(t *testing.T) { + t.Parallel() + require := require.New(t) + assert := assert.New(t) + var hub1 *Hub + var hub2 *Hub + var server1 *httptest.Server + var server2 *httptest.Server + if isLocalTest(t) { + hub1, _, _, server1 = CreateHubForTest(t) + + hub2 = hub1 + server2 = server1 + } else { + hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) + } + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId)) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + roomId := "test-room" + _, err = client1.JoinRoom(ctx, roomId) + require.NoError(err) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) client2 := NewTestClient(t, server2, hub2) defer client2.CloseWithBye() require.NoError(client2.SendHelloInternal()) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) session2 := hub2.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) - MustSucceed2(t, client2.JoinRoom, ctx, roomId) + _, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) - client1.RunUntilJoined(ctx, hello2.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello2.Hello)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - if msg, ok := checkMessageParticipantsInCall(t, msg); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { if assert.Len(msg.Users, 1) { assert.Equal(true, msg.Users[0]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) assert.EqualValues(3, msg.Users[0]["inCall"], "%+v", msg) } } } - _, unexpected, _ := client2.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello) + _, unexpected, err := client2.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello) + assert.NoError(err) if len(unexpected) == 0 { - if msg, ok := client2.RunUntilMessage(ctx); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { unexpected = append(unexpected, msg) } } require.Len(unexpected, 1) - if msg, ok := checkMessageParticipantsInCall(t, unexpected[0]); ok { + if msg, err := checkMessageParticipantsInCall(unexpected[0]); assert.NoError(err) { if assert.Len(msg.Users, 1) { assert.Equal(true, msg.Users[0]["internal"]) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[0]["sessionId"]) + assert.Equal(hello2.Hello.SessionId, msg.Users[0]["sessionId"]) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[0]["inCall"]) } } calledCtx, calledCancel := context.WithTimeout(ctx, time.Second) - virtualSessionId := api.PublicSessionId("virtual-session-id") + virtualSessionId := "virtual-session-id" virtualUserId := "virtual-user-id" generatedSessionId := GetVirtualSessionId(session2, virtualSessionId) - setSessionRequestHandler(t, func(request *talk.BackendClientSessionRequest) { + setSessionRequestHandler(t, func(request *BackendClientSessionRequest) { defer calledCancel() assert.Equal("add", request.Action, "%+v", request) assert.Equal(roomId, request.RoomId, "%+v", request) @@ -4349,8 +4786,8 @@ func TestVirtualClientSessions(t *testing.T) { assert.Equal(virtualUserId, request.UserId, "%+v", request) }) - require.NoError(client2.SendInternalAddSession(&api.AddSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ + require.NoError(client2.SendInternalAddSession(&AddSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ SessionId: virtualSessionId, RoomId: roomId, }, @@ -4370,52 +4807,52 @@ func TestVirtualClientSessions(t *testing.T) { virtualSession := virtualSessions[0] - if msg, ok := client1.RunUntilMessage(ctx); ok { - client1.checkMessageJoinedSession(msg, virtualSession.PublicId(), virtualUserId) + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkMessageJoinedSession(msg, virtualSession.PublicId(), virtualUserId)) } - if msg, ok := client1.RunUntilMessage(ctx); ok { - if msg, ok := checkMessageParticipantsInCall(t, msg); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { if assert.Len(msg.Users, 2) { assert.Equal(true, msg.Users[0]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[0]["inCall"], "%+v", msg) assert.Equal(true, msg.Users[1]["virtual"], "%+v", msg) - assert.EqualValues(virtualSession.PublicId(), msg.Users[1]["sessionId"], "%+v", msg) + assert.Equal(virtualSession.PublicId(), msg.Users[1]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithPhone, msg.Users[1]["inCall"], "%+v", msg) } } } - if msg, ok := client1.RunUntilMessage(ctx); ok { - if flags, ok := checkMessageParticipantFlags(t, msg); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if flags, err := checkMessageParticipantFlags(msg); assert.NoError(err) { assert.Equal(roomId, flags.RoomId) assert.Equal(virtualSession.PublicId(), flags.SessionId) assert.EqualValues(FLAG_MUTED_SPEAKING, flags.Flags) } } - if msg, ok := client2.RunUntilMessage(ctx); ok { - client2.checkMessageJoinedSession(msg, virtualSession.PublicId(), virtualUserId) + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client2.checkMessageJoinedSession(msg, virtualSession.PublicId(), virtualUserId)) } - if msg, ok := client2.RunUntilMessage(ctx); ok { - if msg, ok := checkMessageParticipantsInCall(t, msg); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { if assert.Len(msg.Users, 2) { assert.Equal(true, msg.Users[0]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[0]["inCall"], "%+v", msg) assert.Equal(true, msg.Users[1]["virtual"], "%+v", msg) - assert.EqualValues(virtualSession.PublicId(), msg.Users[1]["sessionId"], "%+v", msg) + assert.Equal(virtualSession.PublicId(), msg.Users[1]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithPhone, msg.Users[1]["inCall"], "%+v", msg) } } } - if msg, ok := client2.RunUntilMessage(ctx); ok { - if flags, ok := checkMessageParticipantFlags(t, msg); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + if flags, err := checkMessageParticipantFlags(msg); assert.NoError(err) { assert.Equal(roomId, flags.RoomId) assert.Equal(virtualSession.PublicId(), flags.SessionId) assert.EqualValues(FLAG_MUTED_SPEAKING, flags.Flags) @@ -4423,8 +4860,8 @@ func TestVirtualClientSessions(t *testing.T) { } updatedFlags := uint32(0) - require.NoError(client2.SendInternalUpdateSession(&api.UpdateSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ + require.NoError(client2.SendInternalUpdateSession(&UpdateSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ SessionId: virtualSessionId, RoomId: roomId, }, @@ -4432,16 +4869,16 @@ func TestVirtualClientSessions(t *testing.T) { Flags: &updatedFlags, })) - if msg, ok := client1.RunUntilMessage(ctx); ok { - if flags, ok := checkMessageParticipantFlags(t, msg); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if flags, err := checkMessageParticipantFlags(msg); assert.NoError(err) { assert.Equal(roomId, flags.RoomId) assert.Equal(virtualSession.PublicId(), flags.SessionId) assert.EqualValues(0, flags.Flags) } } - if msg, ok := client2.RunUntilMessage(ctx); ok { - if flags, ok := checkMessageParticipantFlags(t, msg); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + if flags, err := checkMessageParticipantFlags(msg); assert.NoError(err) { assert.Equal(roomId, flags.RoomId) assert.Equal(virtualSession.PublicId(), flags.SessionId) assert.EqualValues(0, flags.Flags) @@ -4450,7 +4887,7 @@ func TestVirtualClientSessions(t *testing.T) { calledCtx, calledCancel = context.WithTimeout(ctx, time.Second) - setSessionRequestHandler(t, func(request *talk.BackendClientSessionRequest) { + setSessionRequestHandler(t, func(request *BackendClientSessionRequest) { defer calledCancel() assert.Equal("remove", request.Action, "%+v", request) assert.Equal(roomId, request.RoomId, "%+v", request) @@ -4459,7 +4896,7 @@ func TestVirtualClientSessions(t *testing.T) { }) // Messages to virtual sessions are sent to the associated client session. - virtualRecipient := api.MessageClientMessageRecipient{ + virtualRecipient := MessageClientMessageRecipient{ Type: "session", SessionId: virtualSession.PublicId(), } @@ -4468,9 +4905,9 @@ func TestVirtualClientSessions(t *testing.T) { client1.SendMessage(virtualRecipient, data) // nolint var payload string - var sender *api.MessageServerMessageSender - var recipient *api.MessageClientMessageRecipient - if checkReceiveClientMessageWithSenderAndRecipient(ctx, t, client2, "session", hello1.Hello, &payload, &sender, &recipient) { + var sender *MessageServerMessageSender + var recipient *MessageClientMessageRecipient + if err := checkReceiveClientMessageWithSenderAndRecipient(ctx, client2, "session", hello1.Hello, &payload, &sender, &recipient); assert.NoError(err) { assert.Equal(virtualSessionId, recipient.SessionId, "%+v", recipient) assert.Equal(data, payload) } @@ -4478,13 +4915,13 @@ func TestVirtualClientSessions(t *testing.T) { data = "control-to-virtual" client1.SendControl(virtualRecipient, data) // nolint - if checkReceiveClientControlWithSenderAndRecipient(ctx, t, client2, "session", hello1.Hello, &payload, &sender, &recipient) { + if err := checkReceiveClientControlWithSenderAndRecipient(ctx, client2, "session", hello1.Hello, &payload, &sender, &recipient); assert.NoError(err) { assert.Equal(virtualSessionId, recipient.SessionId, "%+v", recipient) assert.Equal(data, payload) } - require.NoError(client2.SendInternalRemoveSession(&api.RemoveSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ + require.NoError(client2.SendInternalRemoveSession(&RemoveSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ SessionId: virtualSessionId, RoomId: roomId, }, @@ -4496,19 +4933,19 @@ func TestVirtualClientSessions(t *testing.T) { require.NoError(err) } - if msg, ok := client1.RunUntilMessage(ctx); ok { - client1.checkMessageRoomLeaveSession(msg, virtualSession.PublicId()) + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkMessageRoomLeaveSession(msg, virtualSession.PublicId())) } - if msg, ok := client2.RunUntilMessage(ctx); ok { - client1.checkMessageRoomLeaveSession(msg, virtualSession.PublicId()) + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkMessageRoomLeaveSession(msg, virtualSession.PublicId())) } }) } } func TestDuplicateVirtualSessions(t *testing.T) { - t.Parallel() + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -4530,67 +4967,70 @@ func TestDuplicateVirtualSessions(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId) + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId)) + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) roomId := "test-room" - MustSucceed2(t, client1.JoinRoom, ctx, roomId) + _, err = client1.JoinRoom(ctx, roomId) + require.NoError(err) - client1.RunUntilJoined(ctx, hello1.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) client2 := NewTestClient(t, server2, hub2) defer client2.CloseWithBye() require.NoError(client2.SendHelloInternal()) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) session2 := hub2.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) - MustSucceed2(t, client2.JoinRoom, ctx, roomId) + _, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) - if _, additional, ok := client1.RunUntilJoinedAndReturn(ctx, hello2.Hello); ok { - // TODO: Should not receive participants update before joined event. - if len(additional) == 0 { - if msg, ok := client1.RunUntilMessage(ctx); ok { - additional = append(additional, msg) - } - } + assert.NoError(client1.RunUntilJoined(ctx, hello2.Hello)) - if assert.Len(additional, 1) { - if msg, ok := checkMessageParticipantsInCall(t, additional[0]); ok { - if assert.Len(msg.Users, 1) { - assert.Equal(true, msg.Users[0]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) - assert.EqualValues(3, msg.Users[0]["inCall"], "%+v", msg) - } + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { + if assert.Len(msg.Users, 1) { + assert.Equal(true, msg.Users[0]["internal"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) + assert.EqualValues(3, msg.Users[0]["inCall"], "%+v", msg) } } } - _, unexpected, _ := client2.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello) + _, unexpected, err := client2.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello) + assert.NoError(err) if len(unexpected) == 0 { - if msg, ok := client2.RunUntilMessage(ctx); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { unexpected = append(unexpected, msg) } } require.Len(unexpected, 1) - if msg, ok := checkMessageParticipantsInCall(t, unexpected[0]); ok { + if msg, err := checkMessageParticipantsInCall(unexpected[0]); assert.NoError(err) { if assert.Len(msg.Users, 1) { assert.Equal(true, msg.Users[0]["internal"]) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[0]["sessionId"]) + assert.Equal(hello2.Hello.SessionId, msg.Users[0]["sessionId"]) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[0]["inCall"]) } } calledCtx, calledCancel := context.WithTimeout(ctx, time.Second) - virtualSessionId := api.PublicSessionId("virtual-session-id") + virtualSessionId := "virtual-session-id" virtualUserId := "virtual-user-id" generatedSessionId := GetVirtualSessionId(session2, virtualSessionId) - setSessionRequestHandler(t, func(request *talk.BackendClientSessionRequest) { + setSessionRequestHandler(t, func(request *BackendClientSessionRequest) { defer calledCancel() assert.Equal("add", request.Action, "%+v", request) assert.Equal(roomId, request.RoomId, "%+v", request) @@ -4598,8 +5038,8 @@ func TestDuplicateVirtualSessions(t *testing.T) { assert.Equal(virtualUserId, request.UserId, "%+v", request) }) - require.NoError(client2.SendInternalAddSession(&api.AddSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ + require.NoError(client2.SendInternalAddSession(&AddSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ SessionId: virtualSessionId, RoomId: roomId, }, @@ -4618,63 +5058,63 @@ func TestDuplicateVirtualSessions(t *testing.T) { } virtualSession := virtualSessions[0] - if msg, ok := client1.RunUntilMessage(ctx); ok { - client1.checkMessageJoinedSession(msg, virtualSession.PublicId(), virtualUserId) + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client1.checkMessageJoinedSession(msg, virtualSession.PublicId(), virtualUserId)) } - if msg, ok := client1.RunUntilMessage(ctx); ok { - if msg, ok := checkMessageParticipantsInCall(t, msg); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { if assert.Len(msg.Users, 2) { assert.Equal(true, msg.Users[0]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[0]["inCall"], "%+v", msg) assert.Equal(true, msg.Users[1]["virtual"], "%+v", msg) - assert.EqualValues(virtualSession.PublicId(), msg.Users[1]["sessionId"], "%+v", msg) + assert.Equal(virtualSession.PublicId(), msg.Users[1]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithPhone, msg.Users[1]["inCall"], "%+v", msg) } } } - if msg, ok := client1.RunUntilMessage(ctx); ok { - if flags, ok := checkMessageParticipantFlags(t, msg); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if flags, err := checkMessageParticipantFlags(msg); assert.NoError(err) { assert.Equal(roomId, flags.RoomId) assert.Equal(virtualSession.PublicId(), flags.SessionId) assert.EqualValues(FLAG_MUTED_SPEAKING, flags.Flags) } } - if msg, ok := client2.RunUntilMessage(ctx); ok { - client2.checkMessageJoinedSession(msg, virtualSession.PublicId(), virtualUserId) + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client2.checkMessageJoinedSession(msg, virtualSession.PublicId(), virtualUserId)) } - if msg, ok := client2.RunUntilMessage(ctx); ok { - if msg, ok := checkMessageParticipantsInCall(t, msg); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { if assert.Len(msg.Users, 2) { assert.Equal(true, msg.Users[0]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[0]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[0]["inCall"], "%+v", msg) assert.Equal(true, msg.Users[1]["virtual"], "%+v", msg) - assert.EqualValues(virtualSession.PublicId(), msg.Users[1]["sessionId"], "%+v", msg) + assert.Equal(virtualSession.PublicId(), msg.Users[1]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithPhone, msg.Users[1]["inCall"], "%+v", msg) } } } - if msg, ok := client2.RunUntilMessage(ctx); ok { - if flags, ok := checkMessageParticipantFlags(t, msg); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + if flags, err := checkMessageParticipantFlags(msg); assert.NoError(err) { assert.Equal(roomId, flags.RoomId) assert.Equal(virtualSession.PublicId(), flags.SessionId) assert.EqualValues(FLAG_MUTED_SPEAKING, flags.Flags) } } - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ + InCall: &BackendRoomInCallRequest{ InCall: []byte("0"), - Users: []api.StringMap{ + Users: []map[string]interface{}{ { "sessionId": virtualSession.PublicId(), "participantPermissions": 246, @@ -4683,7 +5123,7 @@ func TestDuplicateVirtualSessions(t *testing.T) { }, { // Request is coming from Nextcloud, so use its session id (which is our "room session id"). - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), + "sessionId": roomId + "-" + hello1.Hello.SessionId, "participantPermissions": 254, "participantType": 1, "lastPing": 234567890, @@ -4701,43 +5141,43 @@ func TestDuplicateVirtualSessions(t *testing.T) { assert.NoError(err) assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - if msg, ok := checkMessageParticipantsInCall(t, msg); ok { + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { if assert.Len(msg.Users, 3) { assert.Equal(true, msg.Users[0]["virtual"], "%+v", msg) - assert.EqualValues(virtualSession.PublicId(), msg.Users[0]["sessionId"], "%+v", msg) + assert.Equal(virtualSession.PublicId(), msg.Users[0]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithPhone, msg.Users[0]["inCall"], "%+v", msg) assert.EqualValues(246, msg.Users[0]["participantPermissions"], "%+v", msg) assert.EqualValues(4, msg.Users[0]["participantType"], "%+v", msg) - assert.EqualValues(hello1.Hello.SessionId, msg.Users[1]["sessionId"], "%+v", msg) + assert.Equal(hello1.Hello.SessionId, msg.Users[1]["sessionId"], "%+v", msg) assert.Nil(msg.Users[1]["inCall"], "%+v", msg) assert.EqualValues(254, msg.Users[1]["participantPermissions"], "%+v", msg) assert.EqualValues(1, msg.Users[1]["participantType"], "%+v", msg) assert.Equal(true, msg.Users[2]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[2]["sessionId"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[2]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[2]["inCall"], "%+v", msg) } } } - if msg, ok := client2.RunUntilMessage(ctx); ok { - if msg, ok := checkMessageParticipantsInCall(t, msg); ok { + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { if assert.Len(msg.Users, 3) { assert.Equal(true, msg.Users[0]["virtual"], "%+v", msg) - assert.EqualValues(virtualSession.PublicId(), msg.Users[0]["sessionId"], "%+v", msg) + assert.Equal(virtualSession.PublicId(), msg.Users[0]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithPhone, msg.Users[0]["inCall"], "%+v", msg) assert.EqualValues(246, msg.Users[0]["participantPermissions"], "%+v", msg) assert.EqualValues(4, msg.Users[0]["participantType"], "%+v", msg) - assert.EqualValues(hello1.Hello.SessionId, msg.Users[1]["sessionId"], "%+v", msg) + assert.Equal(hello1.Hello.SessionId, msg.Users[1]["sessionId"], "%+v", msg) assert.Nil(msg.Users[1]["inCall"], "%+v", msg) assert.EqualValues(254, msg.Users[1]["participantPermissions"], "%+v", msg) assert.EqualValues(1, msg.Users[1]["participantType"], "%+v", msg) assert.Equal(true, msg.Users[2]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[2]["sessionId"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[2]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[2]["inCall"], "%+v", msg) } } @@ -4750,34 +5190,34 @@ func TestDuplicateVirtualSessions(t *testing.T) { defer client3.CloseWithBye() require.NoError(client3.SendHelloResume(hello1.Hello.ResumeId)) - if hello3, ok := client3.RunUntilHello(ctx); ok { + if hello3, err := client3.RunUntilHello(ctx); assert.NoError(err) { assert.Equal(testDefaultUserId, hello3.Hello.UserId, "%+v", hello3.Hello) assert.Equal(hello1.Hello.SessionId, hello3.Hello.SessionId, "%+v", hello3.Hello) assert.Equal(hello1.Hello.ResumeId, hello3.Hello.ResumeId, "%+v", hello3.Hello) } - if msg, ok := client3.RunUntilMessage(ctx); ok { - if msg, ok := checkMessageParticipantsInCall(t, msg); ok { + if msg, err := client3.RunUntilMessage(ctx); assert.NoError(err) { + if msg, err := checkMessageParticipantsInCall(msg); assert.NoError(err) { if assert.Len(msg.Users, 3) { assert.Equal(true, msg.Users[0]["virtual"], "%+v", msg) - assert.EqualValues(virtualSession.PublicId(), msg.Users[0]["sessionId"], "%+v", msg) + assert.Equal(virtualSession.PublicId(), msg.Users[0]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithPhone, msg.Users[0]["inCall"], "%+v", msg) assert.EqualValues(246, msg.Users[0]["participantPermissions"], "%+v", msg) assert.EqualValues(4, msg.Users[0]["participantType"], "%+v", msg) - assert.EqualValues(hello1.Hello.SessionId, msg.Users[1]["sessionId"], "%+v", msg) + assert.Equal(hello1.Hello.SessionId, msg.Users[1]["sessionId"], "%+v", msg) assert.Nil(msg.Users[1]["inCall"], "%+v", msg) assert.EqualValues(254, msg.Users[1]["participantPermissions"], "%+v", msg) assert.EqualValues(1, msg.Users[1]["participantType"], "%+v", msg) assert.Equal(true, msg.Users[2]["internal"], "%+v", msg) - assert.EqualValues(hello2.Hello.SessionId, msg.Users[2]["sessionId"], "%+v", msg) + assert.Equal(hello2.Hello.SessionId, msg.Users[2]["sessionId"], "%+v", msg) assert.EqualValues(FlagInCall|FlagWithAudio, msg.Users[2]["inCall"], "%+v", msg) } } } - setSessionRequestHandler(t, func(request *talk.BackendClientSessionRequest) { + setSessionRequestHandler(t, func(request *BackendClientSessionRequest) { defer calledCancel() assert.Equal("remove", request.Action, "%+v", request) assert.Equal(roomId, request.RoomId, "%+v", request) @@ -4788,7 +5228,8 @@ func TestDuplicateVirtualSessions(t *testing.T) { } } -func DoTestSwitchToOne(t *testing.T, details api.StringMap) { +func DoTestSwitchToOne(t *testing.T, details map[string]interface{}) { + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -4808,48 +5249,53 @@ func DoTestSwitchToOne(t *testing.T, details api.StringMap) { hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) } + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) - roomSessionId1 := api.RoomSessionId("roomsession1") + roomSessionId1 := "roomsession1" roomId1 := "test-room" - roomMsg := MustSucceed3(t, client1.JoinRoomWithRoomSession, ctx, roomId1, roomSessionId1) + roomMsg, err := client1.JoinRoomWithRoomSession(ctx, roomId1, roomSessionId1) + require.NoError(err) require.Equal(roomId1, roomMsg.Room.RoomId) - // TODO: If we join both clients immediately and then afterwards wait for both with - // "WaitForUsersJoined", the clustered test sometimes fails under load because the - // second session receives the remote "joined" event before joining the room itself. - client1.RunUntilJoined(ctx, hello1.Hello) - - roomSessionId2 := api.RoomSessionId("roomsession2") - roomMsg = MustSucceed3(t, client2.JoinRoomWithRoomSession, ctx, roomId1, roomSessionId2) + roomSessionId2 := "roomsession2" + roomMsg, err = client2.JoinRoomWithRoomSession(ctx, roomId1, roomSessionId2) + require.NoError(err) require.Equal(roomId1, roomMsg.Room.RoomId) - client1.RunUntilJoined(ctx, hello2.Hello) - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) roomId2 := "test-room-2" var sessions json.RawMessage - var err error if details != nil { - sessions, err = json.Marshal(map[api.RoomSessionId]any{ + sessions, err = json.Marshal(map[string]interface{}{ roomSessionId1: details, }) require.NoError(err) } else { - sessions, err = json.Marshal([]api.RoomSessionId{ + sessions, err = json.Marshal([]string{ roomSessionId1, }) require.NoError(err) } // Notify first client to switch to different room. - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "switchto", - SwitchTo: &talk.BackendRoomSwitchToMessageRequest{ + SwitchTo: &BackendRoomSwitchToMessageRequest{ RoomId: roomId2, Sessions: sessions, }, @@ -4869,30 +5315,34 @@ func DoTestSwitchToOne(t *testing.T, details api.StringMap) { detailsData, err = json.Marshal(details) require.NoError(err) } - client1.RunUntilSwitchTo(ctx, roomId2, detailsData) + _, err = client1.RunUntilSwitchTo(ctx, roomId2, detailsData) + assert.NoError(err) // The other client will not receive a message. ctx2, cancel2 := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel2() - client2.RunUntilErrorIs(ctx2, ErrNoMessageReceived, context.DeadlineExceeded) + if message, err := client2.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no message, got %+v", message) + } else if err != ErrNoMessageReceived && err != context.DeadlineExceeded { + assert.NoError(err) + } }) } } func TestSwitchToOneMap(t *testing.T) { - t.Parallel() - DoTestSwitchToOne(t, api.StringMap{ + DoTestSwitchToOne(t, map[string]interface{}{ "foo": "bar", }) } func TestSwitchToOneList(t *testing.T) { - t.Parallel() DoTestSwitchToOne(t, nil) } -func DoTestSwitchToMultiple(t *testing.T, details1 api.StringMap, details2 api.StringMap) { +func DoTestSwitchToMultiple(t *testing.T, details1 map[string]interface{}, details2 map[string]interface{}) { + CatchLogForTest(t) for _, subtest := range clusteredTests { t.Run(subtest, func(t *testing.T) { t.Parallel() @@ -4912,51 +5362,54 @@ func DoTestSwitchToMultiple(t *testing.T, details1 api.StringMap, details2 api.S hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) } + client1 := NewTestClient(t, server1, hub1) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + client2 := NewTestClient(t, server2, hub2) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - defer client1.CloseWithBye() - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") - defer client2.CloseWithBye() + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) - roomSessionId1 := api.RoomSessionId("roomsession1") + roomSessionId1 := "roomsession1" roomId1 := "test-room" - roomMsg := MustSucceed3(t, client1.JoinRoomWithRoomSession, ctx, roomId1, roomSessionId1) + roomMsg, err := client1.JoinRoomWithRoomSession(ctx, roomId1, roomSessionId1) + require.NoError(err) require.Equal(roomId1, roomMsg.Room.RoomId) - // TODO: If we join both clients immediately and then afterwards wait for both with - // "WaitForUsersJoined", the clustered test sometimes fails under load because the - // second session receives the remote "joined" event before joining the room itself. - client1.RunUntilJoined(ctx, hello1.Hello) - - roomSessionId2 := api.RoomSessionId("roomsession2") - roomMsg = MustSucceed3(t, client2.JoinRoomWithRoomSession, ctx, roomId1, roomSessionId2) + roomSessionId2 := "roomsession2" + roomMsg, err = client2.JoinRoomWithRoomSession(ctx, roomId1, roomSessionId2) + require.NoError(err) require.Equal(roomId1, roomMsg.Room.RoomId) - client1.RunUntilJoined(ctx, hello2.Hello) - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) roomId2 := "test-room-2" var sessions json.RawMessage - var err error if details1 != nil || details2 != nil { - sessions, err = json.Marshal(map[api.RoomSessionId]any{ + sessions, err = json.Marshal(map[string]interface{}{ roomSessionId1: details1, roomSessionId2: details2, }) require.NoError(err) } else { - sessions, err = json.Marshal([]api.RoomSessionId{ + sessions, err = json.Marshal([]string{ roomSessionId1, roomSessionId2, }) require.NoError(err) } - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "switchto", - SwitchTo: &talk.BackendRoomSwitchToMessageRequest{ + SwitchTo: &BackendRoomSwitchToMessageRequest{ RoomId: roomId2, Sessions: sessions, }, @@ -4976,41 +5429,41 @@ func DoTestSwitchToMultiple(t *testing.T, details1 api.StringMap, details2 api.S detailsData1, err = json.Marshal(details1) require.NoError(err) } - client1.RunUntilSwitchTo(ctx, roomId2, detailsData1) + _, err = client1.RunUntilSwitchTo(ctx, roomId2, detailsData1) + assert.NoError(err) var detailsData2 json.RawMessage if details2 != nil { detailsData2, err = json.Marshal(details2) require.NoError(err) } - client2.RunUntilSwitchTo(ctx, roomId2, detailsData2) + _, err = client2.RunUntilSwitchTo(ctx, roomId2, detailsData2) + assert.NoError(err) }) } } func TestSwitchToMultipleMap(t *testing.T) { - t.Parallel() - DoTestSwitchToMultiple(t, api.StringMap{ + DoTestSwitchToMultiple(t, map[string]interface{}{ "foo": "bar", - }, api.StringMap{ + }, map[string]interface{}{ "bar": "baz", }) } func TestSwitchToMultipleList(t *testing.T) { - t.Parallel() DoTestSwitchToMultiple(t, nil, nil) } func TestSwitchToMultipleMixed(t *testing.T) { - t.Parallel() - DoTestSwitchToMultiple(t, api.StringMap{ + DoTestSwitchToMultiple(t, map[string]interface{}{ "foo": "bar", }, nil) } func TestGeoipOverrides(t *testing.T) { t.Parallel() + CatchLogForTest(t) assert := assert.New(t) country1 := "DE" country2 := "IT" @@ -5027,17 +5480,16 @@ func TestGeoipOverrides(t *testing.T) { return conf, err }) - assert.Equal(geoip.Loopback, hub.LookupCountry("127.0.0.1")) - assert.Equal(geoip.UnknownCountry, hub.LookupCountry("8.8.8.8")) - assert.EqualValues(country1, hub.LookupCountry("10.1.1.2")) - assert.EqualValues(country2, hub.LookupCountry("10.2.1.2")) - assert.EqualValues(strings.ToUpper(country3), hub.LookupCountry("192.168.10.20")) + assert.Equal(loopback, hub.OnLookupCountry(&Client{addr: "127.0.0.1"})) + assert.Equal(unknownCountry, hub.OnLookupCountry(&Client{addr: "8.8.8.8"})) + assert.Equal(country1, hub.OnLookupCountry(&Client{addr: "10.1.1.2"})) + assert.Equal(country2, hub.OnLookupCountry(&Client{addr: "10.2.1.2"})) + assert.Equal(strings.ToUpper(country3), hub.OnLookupCountry(&Client{addr: "192.168.10.20"})) } func TestDialoutStatus(t *testing.T) { t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) require := require.New(t) assert := assert.New(t) _, _, _, hub, _, server := CreateBackendServerForTest(t) @@ -5046,16 +5498,25 @@ func TestDialoutStatus(t *testing.T) { defer internalClient.CloseWithBye() require.NoError(internalClient.SendHelloInternalWithFeatures([]string{"start-dialout"})) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - MustSucceed1(t, internalClient.RunUntilHello, ctx) + _, err := internalClient.RunUntilHello(ctx) + require.NoError(err) roomId := "12345" - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() - MustSucceed2(t, client.JoinRoom, ctx, roomId) - client.RunUntilJoined(ctx, hello.Hello) + require.NoError(client.SendHello(testDefaultUserId)) + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + + _, err = client.JoinRoom(ctx, roomId) + require.NoError(err) + + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) callId := "call-123" @@ -5063,8 +5524,8 @@ func TestDialoutStatus(t *testing.T) { go func(client *TestClient) { defer close(stopped) - msg, ok := client.RunUntilMessage(ctx) - if !ok { + msg, err := client.RunUntilMessage(ctx) + if !assert.NoError(err) { return } @@ -5077,15 +5538,15 @@ func TestDialoutStatus(t *testing.T) { assert.Equal(roomId, msg.Internal.Dialout.RoomId) - response := &api.ClientMessage{ + response := &ClientMessage{ Id: msg.Id, Type: "internal", - Internal: &api.InternalClientMessage{ + Internal: &InternalClientMessage{ Type: "dialout", - Dialout: &api.DialoutInternalClientMessage{ + Dialout: &DialoutInternalClientMessage{ Type: "status", RoomId: msg.Internal.Dialout.RoomId, - Status: &api.DialoutStatusInternalClientMessage{ + Status: &DialoutStatusInternalClientMessage{ Status: "accepted", CallId: callId, }, @@ -5099,9 +5560,9 @@ func TestDialoutStatus(t *testing.T) { <-stopped }() - msg := &talk.BackendServerRoomRequest{ + msg := &BackendServerRoomRequest{ Type: "dialout", - Dialout: &talk.BackendRoomDialoutRequest{ + Dialout: &BackendRoomDialoutRequest{ Number: "+1234567890", }, } @@ -5115,7 +5576,7 @@ func TestDialoutStatus(t *testing.T) { assert.NoError(err) require.Equal(http.StatusOK, res.StatusCode, "Expected success, got %s", string(body)) - var response talk.BackendServerRoomResponse + var response BackendServerRoomResponse if assert.NoError(json.Unmarshal(body, &response)) { assert.Equal("dialout", response.Type) if assert.NotNil(response.Dialout) { @@ -5125,30 +5586,30 @@ func TestDialoutStatus(t *testing.T) { } key := "callstatus_" + callId - if msg, ok := client.RunUntilMessage(ctx); ok { - checkMessageTransientInitialOrSet(t, msg, key, api.StringMap{ + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(checkMessageTransientSet(msg, key, map[string]interface{}{ "callid": callId, "status": "accepted", - }) + }, nil)) } - require.NoError(internalClient.SendInternalDialout(&api.DialoutInternalClientMessage{ + require.NoError(internalClient.SendInternalDialout(&DialoutInternalClientMessage{ RoomId: roomId, Type: "status", - Status: &api.DialoutStatusInternalClientMessage{ + Status: &DialoutStatusInternalClientMessage{ CallId: callId, Status: "ringing", }, })) - if msg, ok := client.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, key, api.StringMap{ + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(checkMessageTransientSet(msg, key, map[string]interface{}{ "callid": callId, "status": "ringing", - }, api.StringMap{ + }, map[string]interface{}{ "callid": callId, "status": "accepted", - }) + })) } old := removeCallStatusTTL @@ -5158,41 +5619,42 @@ func TestDialoutStatus(t *testing.T) { removeCallStatusTTL = 500 * time.Millisecond clearedCause := "cleared-call" - require.NoError(internalClient.SendInternalDialout(&api.DialoutInternalClientMessage{ + require.NoError(internalClient.SendInternalDialout(&DialoutInternalClientMessage{ RoomId: roomId, Type: "status", - Status: &api.DialoutStatusInternalClientMessage{ + Status: &DialoutStatusInternalClientMessage{ CallId: callId, Status: "cleared", Cause: clearedCause, }, })) - if msg, ok := client.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, key, api.StringMap{ + if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(checkMessageTransientSet(msg, key, map[string]interface{}{ "callid": callId, "status": "cleared", "cause": clearedCause, - }, api.StringMap{ + }, map[string]interface{}{ "callid": callId, "status": "ringing", - }) + })) } ctx2, cancel := context.WithTimeout(ctx, removeCallStatusTTL*2) defer cancel() - if msg, ok := client.RunUntilMessage(ctx2); ok { - checkMessageTransientRemove(t, msg, key, api.StringMap{ + if msg, err := client.RunUntilMessage(ctx2); assert.NoError(err) { + assert.NoError(checkMessageTransientRemove(msg, key, map[string]interface{}{ "callid": callId, "status": "cleared", "cause": clearedCause, - }) + })) } } func TestGracefulShutdownInitial(t *testing.T) { t.Parallel() + CatchLogForTest(t) hub, _, _, _ := CreateHubForTest(t) hub.ScheduleShutdown() @@ -5201,13 +5663,21 @@ func TestGracefulShutdownInitial(t *testing.T) { func TestGracefulShutdownOnBye(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, _ := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + + _, err := client.RunUntilHello(ctx) + require.NoError(err) hub.ScheduleShutdown() select { @@ -5227,13 +5697,21 @@ func TestGracefulShutdownOnBye(t *testing.T) { func TestGracefulShutdownOnExpiration(t *testing.T) { t.Parallel() + CatchLogForTest(t) + require := require.New(t) assert := assert.New(t) hub, _, _, server := CreateHubForTest(t) ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - client, _ := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + + _, err := client.RunUntilHello(ctx) + require.NoError(err) hub.ScheduleShutdown() select { @@ -5249,7 +5727,7 @@ func TestGracefulShutdownOnExpiration(t *testing.T) { case <-time.After(100 * time.Millisecond): } - hub.performHousekeeping(time.Now().Add(sessionExpireDuration + time.Second)) + performHousekeeping(hub, time.Now().Add(sessionExpireDuration+time.Second)) select { case <-hub.ShutdownChannel(): @@ -5257,53 +5735,3 @@ func TestGracefulShutdownOnExpiration(t *testing.T) { assert.Fail("should have shutdown") } } - -func TestHubGetPublisherIdForSessionId(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - mcu := sfutest.NewSFU(t) - hub.SetMcu(mcu) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - done := make(chan struct{}) - go func() { - defer close(done) - - if reply, err := hub.GetPublisherIdForSessionId(ctx, hello.Hello.SessionId, sfu.StreamTypeVideo); assert.NoError(err) && assert.NotNil(reply) { - assert.Equal("https://proxy.domain.invalid", reply.ProxyUrl) - assert.Equal("10.20.30.40", reply.Ip) - // The test-SFU doesn't support token creation. - assert.Empty(reply.ConnectToken) - assert.Empty(reply.PublisherToken) - - if session := hub.GetSessionByPublicId(hello.Hello.SessionId); assert.NotNil(session) { - if cs, ok := session.(*ClientSession); assert.True(ok) { - if pub := cs.GetPublisher(sfu.StreamTypeVideo); assert.NotNil(pub) { - assert.EqualValues(pub.PublisherId(), reply.PublisherId) - } - } - } - } - }() - - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "54321", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - })) - - <-done -} diff --git a/internal/as_error.go b/internal/as_error.go deleted file mode 100644 index a7ab70e..0000000 --- a/internal/as_error.go +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "errors" -) - -// AsErrorType finds the first error in err's tree that matches the type E, -// and if one is found, returns that error value and true. Otherwise, it -// returns the zero value of E and false. -// -// The tree consists of err itself, followed by the errors obtained by -// repeatedly calling its Unwrap() error or Unwrap() []error method. -// When err wraps multiple errors, AsErrorType examines err followed by a -// depth-first traversal of its children. -// -// An error err matches the type E if the type assertion err.(E) holds, -// or if the error has a method As(any) bool such that err.As(target) -// returns true when target is a non-nil *E. In the latter case, the As -// method is responsible for setting target. -func AsErrorType[E error](err error) (E, bool) { - var e E - if err == nil { - return e, false - } else if errors.As(err, &e) { - return e, true - } - - return e, false -} diff --git a/internal/as_error_test.go b/internal/as_error_test.go deleted file mode 100644 index 5fc2564..0000000 --- a/internal/as_error_test.go +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -type testError struct{} - -func (e testError) Error() string { - return "test error" -} - -func TestAsErrorType(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - - if e, ok := AsErrorType[*testError](nil); assert.False(ok) { - assert.Nil(e) - } - - err1 := &testError{} - if e, ok := AsErrorType[*testError](err1); assert.True(ok) { - assert.Same(err1, e) - } - - err2 := errors.New("other error") - if e, ok := AsErrorType[*testError](err2); assert.False(ok) { - assert.Nil(e) - } - - err3 := fmt.Errorf("wrapped error: %w", err1) - if e, ok := AsErrorType[*testError](err3); assert.True(ok) { - assert.Same(err1, e) - } -} diff --git a/internal/canonicalize_url.go b/internal/canonicalize_url.go deleted file mode 100644 index dabdf31..0000000 --- a/internal/canonicalize_url.go +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "net/url" - "strings" -) - -func hasStandardPort(u *url.URL) bool { - switch u.Scheme { - case "http": - return u.Port() == "80" - case "https": - return u.Port() == "443" - default: - return false - } -} - -func CanonicalizeUrl(u *url.URL) (*url.URL, bool) { - var changed bool - if strings.Contains(u.Host, ":") && hasStandardPort(u) { - u.Host = u.Hostname() - changed = true - } - return u, changed -} - -func CanonicalizeUrlString(s string) (string, error) { - u, err := url.Parse(s) - if err != nil { - return s, err - } - - if strings.Contains(u.Host, ":") && hasStandardPort(u) { - u.Host = u.Hostname() - s = u.String() - } - return s, nil -} diff --git a/internal/canonicalize_url_test.go b/internal/canonicalize_url_test.go deleted file mode 100644 index 9c9b1a1..0000000 --- a/internal/canonicalize_url_test.go +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCanonicalizeUrl(t *testing.T) { - t.Parallel() - mustParse := func(s string) *url.URL { - t.Helper() - u, err := url.Parse(s) - require.NoError(t, err) - return u - } - testcases := []struct { - url *url.URL - expected *url.URL - }{ - { - url: mustParse("http://server.domain.tld:80/foo/"), - expected: mustParse("http://server.domain.tld/foo/"), - }, - { - url: mustParse("http://server.domain.tld:81/foo/"), - expected: mustParse("http://server.domain.tld:81/foo/"), - }, - { - url: mustParse("https://server.domain.tld:443/foo/"), - expected: mustParse("https://server.domain.tld/foo/"), - }, - { - url: mustParse("https://server.domain.tld:444/foo/"), - expected: mustParse("https://server.domain.tld:444/foo/"), - }, - { - url: mustParse("foo://server.domain.tld:443/foo/"), - expected: mustParse("foo://server.domain.tld:443/foo/"), - }, - } - - assert := assert.New(t) - for idx, tc := range testcases { - expectChanged := tc.url.String() != tc.expected.String() - canonicalized, changed := CanonicalizeUrl(tc.url) - assert.Equal(tc.url, canonicalized) // urls will be changed inplace - if !expectChanged { - assert.False(changed, "testcase %d should not have changed the url", idx) - continue - } - - if assert.True(changed, "testcase %d: should have changed the url", idx) { - assert.Equal(tc.expected, canonicalized, "testcase %d failed", idx) - } - } -} - -func TestCanonicalizeUrlString(t *testing.T) { - t.Parallel() - testcases := []struct { - s string - expected string - err string - }{ - { - s: "http://server.domain.tld:80/foo/", - expected: "http://server.domain.tld/foo/", - }, - { - s: "http://server.domain.tld:81/foo/", - expected: "http://server.domain.tld:81/foo/", - }, - { - s: "https://server.domain.tld:443/foo/", - expected: "https://server.domain.tld/foo/", - }, - { - s: "https://server.domain.tld:444/foo/", - expected: "https://server.domain.tld:444/foo/", - }, - { - s: "foo://server.domain.tld:443/foo/", - expected: "foo://server.domain.tld:443/foo/", - }, - { - s: "://server.domain.tld:443/foo/", - err: "missing protocol", - }, - } - - assert := assert.New(t) - for idx, tc := range testcases { - canonicalized, err := CanonicalizeUrlString(tc.s) - if tc.err != "" { - assert.ErrorContains(err, tc.err, "testcase %d failed", idx) - continue - } - - if assert.NoError(err, "testcase %d: should not have failed", idx) { - assert.Equal(tc.expected, canonicalized, "testcase %d failed", idx) - } - } -} diff --git a/internal/crypto_helpers_test.go b/internal/crypto_helpers_test.go deleted file mode 100644 index e52b168..0000000 --- a/internal/crypto_helpers_test.go +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "os" - "path" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenerateSelfSignedCertificateForTesting(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - bits := 1024 - key, err := rsa.GenerateKey(rand.Reader, bits) - require.NoError(err) - cert := GenerateSelfSignedCertificateForTesting(t, "Testing", key) - require.NotNil(cert) - - if assert.Len(cert.Subject.Organization, 1) { - assert.Equal("Testing", cert.Subject.Organization[0]) - } - if assert.Len(cert.DNSNames, 1) { - assert.Equal("localhost", cert.DNSNames[0]) - } - if assert.Len(cert.IPAddresses, 1) { - assert.Equal("127.0.0.1", cert.IPAddresses[0].String()) - } - if assert.IsType(&rsa.PublicKey{}, cert.PublicKey) { - pkey := cert.PublicKey.(*rsa.PublicKey) - assert.Equal(bits/8, pkey.Size()) - } -} - -func TestWriteKeys(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - key, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - - dir := t.TempDir() - privateFilename := path.Join(dir, "testing.key") - if assert.NoError(WritePrivateKey(key, privateFilename)) { - if data, err := os.ReadFile(privateFilename); assert.NoError(err) { - if block, rest := pem.Decode(data); assert.Equal("RSA PRIVATE KEY", block.Type) && assert.Empty(rest) { - if parsed, err := x509.ParsePKCS1PrivateKey(block.Bytes); assert.NoError(err) { - assert.True(key.Equal(parsed), "keys should be equal") - } - } - } - } - - publicFilename := path.Join(dir, "testing.pem") - if assert.NoError(WritePublicKey(&key.PublicKey, publicFilename)) { - if data, err := os.ReadFile(publicFilename); assert.NoError(err) { - if block, rest := pem.Decode(data); assert.Equal("RSA PUBLIC KEY", block.Type) && assert.Empty(rest) { - if parsed, err := x509.ParsePKIXPublicKey(block.Bytes); assert.NoError(err) { - assert.True(key.PublicKey.Equal(parsed), "keys should be equal") - } - } - } - } -} - -func TestReplaceCertificate(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - bits := 1024 - key, err := rsa.GenerateKey(rand.Reader, bits) - require.NoError(err) - cert1 := GenerateSelfSignedCertificateForTesting(t, "Testing", key) - require.NotNil(cert1) - - dir := t.TempDir() - filename := path.Join(dir, "testing.crt") - require.NoError(WriteCertificate(cert1, filename)) - stat1, err := os.Stat(filename) - require.NoError(err) - - cert2 := GenerateSelfSignedCertificateForTesting(t, "Testing", key) - require.NotNil(cert2) - ReplaceCertificate(t, filename, cert2) - stat2, err := os.Stat(filename) - require.NoError(err) - - assert.NotEqual(stat1.ModTime(), stat2.ModTime()) -} diff --git a/internal/ips.go b/internal/ips.go deleted file mode 100644 index 94f4484..0000000 --- a/internal/ips.go +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "net" -) - -func IsLoopbackIP(addr string) bool { - ip := net.ParseIP(addr) - if len(ip) == 0 { - return false - } - - return ip.IsLoopback() -} - -func IsPrivateIP(addr string) bool { - ip := net.ParseIP(addr) - if len(ip) == 0 { - return false - } - - return ip.IsPrivate() -} diff --git a/internal/ips_test.go b/internal/ips_test.go deleted file mode 100644 index 804e58f..0000000 --- a/internal/ips_test.go +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestIsLoopbackIP(t *testing.T) { - t.Parallel() - loopback := []string{ - "127.0.0.1", - "127.1.0.1", - "::1", - "::ffff:127.0.0.1", - } - nonLoopback := []string{ - "", - "invalid", - "1.2.3.4", - "::0", - "::2", - } - assert := assert.New(t) - for _, ip := range loopback { - assert.True(IsLoopbackIP(ip), "should be loopback: %s", ip) - } - for _, ip := range nonLoopback { - assert.False(IsLoopbackIP(ip), "should not be loopback: %s", ip) - } -} - -func TestIsPrivateIP(t *testing.T) { - t.Parallel() - private := []string{ - "10.1.2.3", - "172.20.21.22", - "192.168.10.20", - "fdea:aef9:06e3:bb24:1234:1234:1234:1234", - "fd12:3456:789a:1::1", - } - nonPrivate := []string{ - "", - "invalid", - "127.0.0.1", - "1.2.3.4", - "::0", - "::1", - "::2", - "1234:3456:789a:1::1", - } - assert := assert.New(t) - for _, ip := range private { - assert.True(IsPrivateIP(ip), "should be private: %s", ip) - } - for _, ip := range nonPrivate { - assert.False(IsPrivateIP(ip), "should not be private: %s", ip) - } -} diff --git a/internal/make_ptr.go b/internal/make_ptr.go deleted file mode 100644 index b262aa6..0000000 --- a/internal/make_ptr.go +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -func MakePtr[T any](v T) *T { - return &v -} diff --git a/internal/make_ptr_test.go b/internal/make_ptr_test.go deleted file mode 100644 index 1b5874e..0000000 --- a/internal/make_ptr_test.go +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_MakePtr(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - if v := MakePtr(10); assert.NotNil(v) { - assert.Equal(10, *v) - } - - if v := MakePtr("foo"); assert.NotNil(v) { - assert.Equal("foo", *v) - } -} diff --git a/internal/nocopy.go b/internal/nocopy.go deleted file mode 100644 index cc81cc3..0000000 --- a/internal/nocopy.go +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -// NoCopy may be added to structs which must not be copied -// after the first use. -// -// See https://golang.org/issues/8005#issuecomment-190753527 -// for details. -// -// Note that it must not be embedded, due to the Lock and Unlock methods. -type NoCopy struct{} - -// Lock is a no-op used by -copylocks checker from `go vet`. -func (*NoCopy) Lock() {} -func (*NoCopy) Unlock() {} diff --git a/internal/random_string.go b/internal/random_string.go deleted file mode 100644 index 9221823..0000000 --- a/internal/random_string.go +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "crypto/rand" -) - -func RandomString(size int) string { - s := rand.Text() - for len(s) < size { - s += rand.Text() - } - return s[:size] -} diff --git a/internal/random_string_test.go b/internal/random_string_test.go deleted file mode 100644 index 78aca48..0000000 --- a/internal/random_string_test.go +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRandomString(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - s1 := RandomString(10) - assert.Len(s1, 10) - assert.NotEqual(s1, RandomString(10)) - - s2 := RandomString(123) - assert.Len(s2, 123) - assert.NotEqual(s2, RandomString(123)) -} diff --git a/internal/split_entries.go b/internal/split_entries.go deleted file mode 100644 index 3a6b767..0000000 --- a/internal/split_entries.go +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "iter" - "strings" -) - -// SplitEntries returns an iterator over all non-empty substrings of s separated -// by sep. -func SplitEntries(s string, sep string) iter.Seq[string] { - return func(yield func(entry string) bool) { - for entry := range strings.SplitSeq(s, sep) { - if entry = strings.TrimSpace(entry); entry != "" { - if !yield(entry) { - return - } - } - } - } -} diff --git a/internal/split_entries_test.go b/internal/split_entries_test.go deleted file mode 100644 index 0e77147..0000000 --- a/internal/split_entries_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "slices" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSplitEntries(t *testing.T) { - t.Parallel() - assert := assert.New(t) - testcases := []struct { - s string - sep string - expected []string - }{ - { - "a b", - " ", - []string{"a", "b"}, - }, - { - "a b", - ",", - []string{"a b"}, - }, - { - "a b", - " ", - []string{"a", "b"}, - }, - { - "a,b,", - ",", - []string{"a", "b"}, - }, - { - "a,,b", - ",", - []string{"a", "b"}, - }, - } - for idx, tc := range testcases { - assert.Equal(tc.expected, slices.Collect(SplitEntries(tc.s, tc.sep)), "failed for testcase %d: %s", idx, tc.s) - } -} diff --git a/sfu/janus/janus/janus.go b/janus_client.go similarity index 76% rename from sfu/janus/janus/janus.go rename to janus_client.go index bf45b90..5716bef 100644 --- a/sfu/janus/janus/janus.go +++ b/janus_client.go @@ -26,13 +26,12 @@ * * Added error handling and improve functionality. */ -package janus +package signaling import ( "bytes" "context" "encoding/json" - "errors" "fmt" "log" "net/http" @@ -43,9 +42,6 @@ import ( "github.com/gorilla/websocket" "github.com/notedit/janus-go" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" ) const ( @@ -123,51 +119,24 @@ const ( var ( janusDialer = websocket.Dialer{ - Subprotocols: []string{"janus-protocol"}, - Proxy: http.ProxyFromEnvironment, - WriteBufferPool: &sync.Pool{}, + Subprotocols: []string{"janus-protocol"}, + Proxy: http.ProxyFromEnvironment, } ) -var msgtypes = map[string]func() any{ - "error": func() any { return &janus.ErrorMsg{} }, - "success": func() any { return &janus.SuccessMsg{} }, - "detached": func() any { return &janus.DetachedMsg{} }, - "server_info": func() any { return &InfoMsg{} }, - "ack": func() any { return &janus.AckMsg{} }, - "event": func() any { return &janus.EventMsg{} }, - "webrtcup": func() any { return &janus.WebRTCUpMsg{} }, - "media": func() any { return &janus.MediaMsg{} }, - "hangup": func() any { return &janus.HangupMsg{} }, - "slowlink": func() any { return &janus.SlowLinkMsg{} }, - "timeout": func() any { return &janus.TimeoutMsg{} }, - "trickle": func() any { return &TrickleMsg{} }, -} - -type ( - EventMsg = janus.EventMsg - HangupMsg = janus.HangupMsg - DetachedMsg = janus.DetachedMsg - WebRTCUpMsg = janus.WebRTCUpMsg - SlowLinkMsg = janus.SlowLinkMsg - MediaMsg = janus.MediaMsg - AckMsg = janus.AckMsg - SuccessMsg = janus.SuccessMsg - SuccessData = janus.SuccessData - ErrorMsg = janus.ErrorMsg - ErrorData = janus.ErrorData - - PluginInfo = janus.PluginInfo - PluginData = janus.PluginData -) - -type InfoDependencies struct { - Glib2 string `json:"glib2"` - Jansson string `json:"jansson"` - Libnice string `json:"libnice"` - Libsrtp string `json:"libsrtp"` - Libcurl string `json:"libcurl,omitempty"` - Crypto string `json:"crypto"` +var msgtypes = map[string]func() interface{}{ + "error": func() interface{} { return &janus.ErrorMsg{} }, + "success": func() interface{} { return &janus.SuccessMsg{} }, + "detached": func() interface{} { return &janus.DetachedMsg{} }, + "server_info": func() interface{} { return &InfoMsg{} }, + "ack": func() interface{} { return &janus.AckMsg{} }, + "event": func() interface{} { return &janus.EventMsg{} }, + "webrtcup": func() interface{} { return &janus.WebRTCUpMsg{} }, + "media": func() interface{} { return &janus.MediaMsg{} }, + "hangup": func() interface{} { return &janus.HangupMsg{} }, + "slowlink": func() interface{} { return &janus.SlowLinkMsg{} }, + "timeout": func() interface{} { return &janus.TimeoutMsg{} }, + "trickle": func() interface{} { return &TrickleMsg{} }, } type InfoMsg struct { @@ -176,15 +145,12 @@ type InfoMsg struct { VersionString string `json:"version_string"` Author string DataChannels bool `json:"data_channels"` - EventHandlers bool `json:"event_handlers"` IPv6 bool `json:"ipv6"` LocalIP string `json:"local-ip"` ICE_TCP bool `json:"ice-tcp"` FullTrickle bool `json:"full-trickle"` Transports map[string]janus.PluginInfo Plugins map[string]janus.PluginInfo - Events map[string]janus.PluginInfo - Dependencies InfoDependencies } type TrickleMsg struct { @@ -203,13 +169,13 @@ func unexpected(request string) error { return fmt.Errorf("unexpected response received to '%s' request", request) } -type Transaction struct { - ch chan any - incoming chan any - closer *internal.Closer +type transaction struct { + ch chan interface{} + incoming chan interface{} + closer *Closer } -func (t *Transaction) Run() { +func (t *transaction) run() { for { select { case msg := <-t.incoming: @@ -220,25 +186,25 @@ func (t *Transaction) Run() { } } -func (t *Transaction) Add(msg any) { +func (t *transaction) add(msg interface{}) { t.incoming <- msg } -func (t *Transaction) Quit() { +func (t *transaction) quit() { t.closer.Close() } -func newTransaction() *Transaction { - t := &Transaction{ - ch: make(chan any, 1), - incoming: make(chan any, 8), - closer: internal.NewCloser(), +func newTransaction() *transaction { + t := &transaction{ + ch: make(chan interface{}, 1), + incoming: make(chan interface{}, 8), + closer: NewCloser(), } return t } -func newRequest(method string) (api.StringMap, *Transaction) { - req := make(api.StringMap, 8) +func newRequest(method string) (map[string]interface{}, *transaction) { + req := make(map[string]interface{}, 8) req["janus"] = method return req, newTransaction() } @@ -253,36 +219,33 @@ type dummyGatewayListener struct { func (l *dummyGatewayListener) ConnectionInterrupted() { } -type GatewayInterface interface { +type JanusGatewayInterface interface { Info(context.Context) (*InfoMsg, error) - Create(context.Context) (*Session, error) + Create(context.Context) (*JanusSession, error) Close() error - Send(api.StringMap, *Transaction) (uint64, error) - RemoveTransaction(uint64) + send(map[string]interface{}, *transaction) (uint64, error) + removeTransaction(uint64) - RemoveSession(*Session) + removeSession(*JanusSession) } // Gateway represents a connection to an instance of the Janus Gateway. -type Gateway struct { +type JanusGateway struct { listener GatewayListener // Sessions is a map of the currently active sessions to the gateway. - // +checklocks:Mutex - Sessions map[uint64]*Session + Sessions map[uint64]*JanusSession // Access to the Sessions map should be synchronized with the Gateway.Lock() // and Gateway.Unlock() methods provided by the embedded sync.Mutex. sync.Mutex - // +checklocks:writeMu conn *websocket.Conn nextTransaction atomic.Uint64 - // +checklocks:Mutex - transactions map[uint64]*Transaction + transactions map[uint64]*transaction - closer *internal.Closer + closer *Closer writeMu sync.Mutex } @@ -299,14 +262,14 @@ type Gateway struct { // gateway := new(Gateway) // //gateway.conn = conn -// gateway.transactions = make(map[uint64]chan any) +// gateway.transactions = make(map[uint64]chan interface{}) // gateway.Sessions = make(map[uint64]*JanusSession) // go gateway.recv() // return gateway, nil // } -func NewGateway(ctx context.Context, wsURL string, listener GatewayListener) (*Gateway, error) { +func NewJanusGateway(ctx context.Context, wsURL string, listener GatewayListener) (*JanusGateway, error) { conn, _, err := janusDialer.DialContext(ctx, wsURL, nil) if err != nil { return nil, err @@ -315,12 +278,12 @@ func NewGateway(ctx context.Context, wsURL string, listener GatewayListener) (*G if listener == nil { listener = new(dummyGatewayListener) } - gateway := &Gateway{ + gateway := &JanusGateway{ conn: conn, listener: listener, - transactions: make(map[uint64]*Transaction), - Sessions: make(map[uint64]*Session), - closer: internal.NewCloser(), + transactions: make(map[uint64]*transaction), + Sessions: make(map[uint64]*JanusSession), + closer: NewCloser(), } go gateway.ping() @@ -329,7 +292,7 @@ func NewGateway(ctx context.Context, wsURL string, listener GatewayListener) (*G } // Close closes the underlying connection to the Gateway. -func (gateway *Gateway) Close() error { +func (gateway *JanusGateway) Close() error { gateway.closer.Close() gateway.writeMu.Lock() if gateway.conn == nil { @@ -344,7 +307,7 @@ func (gateway *Gateway) Close() error { return err } -func (gateway *Gateway) cancelTransactions() { +func (gateway *JanusGateway) cancelTransactions() { msg := &janus.ErrorMsg{ Err: janus.ErrorData{ Code: 500, @@ -353,16 +316,16 @@ func (gateway *Gateway) cancelTransactions() { } gateway.Lock() for _, t := range gateway.transactions { - go func(t *Transaction) { - t.Add(msg) - t.Quit() + go func(t *transaction) { + t.add(msg) + t.quit() }(t) } clear(gateway.transactions) gateway.Unlock() } -func (gateway *Gateway) RemoveTransaction(id uint64) { +func (gateway *JanusGateway) removeTransaction(id uint64) { gateway.Lock() t, found := gateway.transactions[id] if found { @@ -370,11 +333,11 @@ func (gateway *Gateway) RemoveTransaction(id uint64) { } gateway.Unlock() if t != nil { - t.Quit() + t.quit() } } -func (gateway *Gateway) Send(msg api.StringMap, t *Transaction) (uint64, error) { +func (gateway *JanusGateway) send(msg map[string]interface{}, t *transaction) (uint64, error) { id := gateway.nextTransaction.Add(1) msg["transaction"] = strconv.FormatUint(id, 10) data, err := json.Marshal(msg) @@ -382,7 +345,7 @@ func (gateway *Gateway) Send(msg api.StringMap, t *Transaction) (uint64, error) return 0, err } - go t.Run() + go t.run() gateway.Lock() gateway.transactions[id] = t gateway.Unlock() @@ -390,24 +353,24 @@ func (gateway *Gateway) Send(msg api.StringMap, t *Transaction) (uint64, error) gateway.writeMu.Lock() if gateway.conn == nil { gateway.writeMu.Unlock() - gateway.RemoveTransaction(id) - return 0, errors.New("not connected") + gateway.removeTransaction(id) + return 0, fmt.Errorf("not connected") } err = gateway.conn.WriteMessage(websocket.TextMessage, data) gateway.writeMu.Unlock() if err != nil { - gateway.RemoveTransaction(id) + gateway.removeTransaction(id) return 0, err } return id, nil } -func passMsg(ch chan any, msg any) { +func passMsg(ch chan interface{}, msg interface{}) { ch <- msg } -func (gateway *Gateway) ping() { +func (gateway *JanusGateway) ping() { ticker := time.NewTicker(time.Second * 30) defer ticker.Stop() @@ -432,7 +395,7 @@ loop: } } -func (gateway *Gateway) recv() { +func (gateway *JanusGateway) recv() { var decodeBuffer bytes.Buffer for { // Read message from Gateway @@ -539,12 +502,12 @@ func (gateway *Gateway) recv() { } // Pass msg - transaction.Add(msg) + transaction.add(msg) } } } -func waitForMessage(ctx context.Context, t *Transaction) (any, error) { +func waitForMessage(ctx context.Context, t *transaction) (interface{}, error) { select { case <-ctx.Done(): return nil, ctx.Err() @@ -555,13 +518,13 @@ func waitForMessage(ctx context.Context, t *Transaction) (any, error) { // Info sends an info request to the Gateway. // On success, an InfoMsg will be returned and error will be nil. -func (gateway *Gateway) Info(ctx context.Context) (*InfoMsg, error) { +func (gateway *JanusGateway) Info(ctx context.Context) (*InfoMsg, error) { req, ch := newRequest("info") - id, err := gateway.Send(req, ch) + id, err := gateway.send(req, ch) if err != nil { return nil, err } - defer gateway.RemoveTransaction(id) + defer gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { @@ -580,13 +543,13 @@ func (gateway *Gateway) Info(ctx context.Context) (*InfoMsg, error) { // Create sends a create request to the Gateway. // On success, a new Session will be returned and error will be nil. -func (gateway *Gateway) Create(ctx context.Context) (*Session, error) { +func (gateway *JanusGateway) Create(ctx context.Context) (*JanusSession, error) { req, ch := newRequest("create") - id, err := gateway.Send(req, ch) + id, err := gateway.send(req, ch) if err != nil { return nil, err } - defer gateway.RemoveTransaction(id) + defer gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { @@ -601,10 +564,10 @@ func (gateway *Gateway) Create(ctx context.Context) (*Session, error) { } // Create new session - session := new(Session) + session := new(JanusSession) session.gateway = gateway session.Id = success.Data.ID - session.Handles = make(map[uint64]*Handle) + session.Handles = make(map[uint64]*JanusHandle) // Store this session gateway.Lock() @@ -614,52 +577,43 @@ func (gateway *Gateway) Create(ctx context.Context) (*Session, error) { return session, nil } -func (gateway *Gateway) RemoveSession(session *Session) { +func (gateway *JanusGateway) removeSession(session *JanusSession) { gateway.Lock() defer gateway.Unlock() delete(gateway.Sessions, session.Id) } // Session represents a session instance on the Janus Gateway. -type Session struct { +type JanusSession struct { // Id is the session_id of this session Id uint64 // Handles is a map of plugin handles within this session - // +checklocks:Mutex - Handles map[uint64]*Handle + Handles map[uint64]*JanusHandle // Access to the Handles map should be synchronized with the Session.Lock() // and Session.Unlock() methods provided by the embedded sync.Mutex. sync.Mutex - gateway GatewayInterface + gateway JanusGatewayInterface } -func NewSession(id uint64, g GatewayInterface) *Session { - return &Session{ - Id: id, - Handles: make(map[uint64]*Handle), - gateway: g, - } -} - -func (session *Session) send(msg api.StringMap, t *Transaction) (uint64, error) { +func (session *JanusSession) send(msg map[string]interface{}, t *transaction) (uint64, error) { msg["session_id"] = session.Id - return session.gateway.Send(msg, t) + return session.gateway.send(msg, t) } // Attach sends an attach request to the Gateway within this session. // plugin should be the unique string of the plugin to attach to. // On success, a new Handle will be returned and error will be nil. -func (session *Session) Attach(ctx context.Context, plugin string) (*Handle, error) { +func (session *JanusSession) Attach(ctx context.Context, plugin string) (*JanusHandle, error) { req, ch := newRequest("attach") req["plugin"] = plugin id, err := session.send(req, ch) if err != nil { return nil, err } - defer session.gateway.RemoveTransaction(id) + defer session.gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { @@ -673,10 +627,10 @@ func (session *Session) Attach(ctx context.Context, plugin string) (*Handle, err return nil, msg } - handle := new(Handle) + handle := new(JanusHandle) handle.session = session handle.Id = success.Data.ID - handle.Events = make(chan any, 8) + handle.Events = make(chan interface{}, 8) session.Lock() session.Handles[handle.Id] = handle @@ -687,13 +641,13 @@ func (session *Session) Attach(ctx context.Context, plugin string) (*Handle, err // KeepAlive sends a keep-alive request to the Gateway. // On success, an AckMsg will be returned and error will be nil. -func (session *Session) KeepAlive(ctx context.Context) (*janus.AckMsg, error) { +func (session *JanusSession) KeepAlive(ctx context.Context) (*janus.AckMsg, error) { req, ch := newRequest("keepalive") id, err := session.send(req, ch) if err != nil { return nil, err } - defer session.gateway.RemoveTransaction(id) + defer session.gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { @@ -712,13 +666,13 @@ func (session *Session) KeepAlive(ctx context.Context) (*janus.AckMsg, error) { // Destroy sends a destroy request to the Gateway to tear down this session. // On success, the Session will be removed from the Gateway.Sessions map, an // AckMsg will be returned and error will be nil. -func (session *Session) Destroy(ctx context.Context) (*janus.AckMsg, error) { +func (session *JanusSession) Destroy(ctx context.Context) (*janus.AckMsg, error) { req, ch := newRequest("destroy") id, err := session.send(req, ch) if err != nil { return nil, err } - defer session.gateway.RemoveTransaction(id) + defer session.gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { @@ -733,36 +687,36 @@ func (session *Session) Destroy(ctx context.Context) (*janus.AckMsg, error) { } // Remove this session from the gateway - session.gateway.RemoveSession(session) + session.gateway.removeSession(session) return ack, nil } // Handle represents a handle to a plugin instance on the Gateway. -type Handle struct { +type JanusHandle struct { // Id is the handle_id of this plugin handle Id uint64 // Type // pub or sub Type string - // User // Userid + //User // Userid User string // Events is a receive only channel that can be used to receive events // related to this handle from the gateway. - Events chan any + Events chan interface{} - session *Session + session *JanusSession } -func (handle *Handle) send(msg api.StringMap, t *Transaction) (uint64, error) { +func (handle *JanusHandle) send(msg map[string]interface{}, t *transaction) (uint64, error) { msg["handle_id"] = handle.Id return handle.session.send(msg, t) } // send sync request -func (handle *Handle) Request(ctx context.Context, body any) (*janus.SuccessMsg, error) { +func (handle *JanusHandle) Request(ctx context.Context, body interface{}) (*janus.SuccessMsg, error) { req, ch := newRequest("message") if body != nil { req["body"] = body @@ -771,7 +725,7 @@ func (handle *Handle) Request(ctx context.Context, body any) (*janus.SuccessMsg, if err != nil { return nil, err } - defer handle.session.gateway.RemoveTransaction(id) + defer handle.session.gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { @@ -791,7 +745,7 @@ func (handle *Handle) Request(ctx context.Context, body any) (*janus.SuccessMsg, // body should be the plugin data to be passed to the plugin, and jsep should // contain an optional SDP offer/answer to establish a WebRTC PeerConnection. // On success, an EventMsg will be returned and error will be nil. -func (handle *Handle) Message(ctx context.Context, body, jsep any) (*janus.EventMsg, error) { +func (handle *JanusHandle) Message(ctx context.Context, body, jsep interface{}) (*janus.EventMsg, error) { req, ch := newRequest("message") if body != nil { req["body"] = body @@ -803,7 +757,7 @@ func (handle *Handle) Message(ctx context.Context, body, jsep any) (*janus.Event if err != nil { return nil, err } - defer handle.session.gateway.RemoveTransaction(id) + defer handle.session.gateway.removeTransaction(id) GetMessage: // No tears.. msg, err := waitForMessage(ctx, ch) @@ -832,14 +786,14 @@ GetMessage: // No tears.. // } // // On success, an AckMsg will be returned and error will be nil. -func (handle *Handle) Trickle(ctx context.Context, candidate any) (*janus.AckMsg, error) { +func (handle *JanusHandle) Trickle(ctx context.Context, candidate interface{}) (*janus.AckMsg, error) { req, ch := newRequest("trickle") req["candidate"] = candidate id, err := handle.send(req, ch) if err != nil { return nil, err } - defer handle.session.gateway.RemoveTransaction(id) + defer handle.session.gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { @@ -859,14 +813,14 @@ func (handle *Handle) Trickle(ctx context.Context, candidate any) (*janus.AckMsg // a new PeerConnection with a plugin. // candidates should be an array of ICE candidates. // On success, an AckMsg will be returned and error will be nil. -func (handle *Handle) TrickleMany(ctx context.Context, candidates any) (*janus.AckMsg, error) { +func (handle *JanusHandle) TrickleMany(ctx context.Context, candidates interface{}) (*janus.AckMsg, error) { req, ch := newRequest("trickle") req["candidates"] = candidates id, err := handle.send(req, ch) if err != nil { return nil, err } - handle.session.gateway.RemoveTransaction(id) + handle.session.gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { @@ -884,13 +838,13 @@ func (handle *Handle) TrickleMany(ctx context.Context, candidates any) (*janus.A // Detach sends a detach request to the Gateway to remove this handle. // On success, an AckMsg will be returned and error will be nil. -func (handle *Handle) Detach(ctx context.Context) (*janus.AckMsg, error) { +func (handle *JanusHandle) Detach(ctx context.Context) (*janus.AckMsg, error) { req, ch := newRequest("detach") id, err := handle.send(req, ch) if err != nil { return nil, err } - defer handle.session.gateway.RemoveTransaction(id) + defer handle.session.gateway.removeTransaction(id) msg, err := waitForMessage(ctx, ch) if err != nil { diff --git a/log/logging.go b/log/logging.go deleted file mode 100644 index cf5cc78..0000000 --- a/log/logging.go +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package log - -import ( - "context" - "log" - "testing" -) - -type loggerKey struct{} - -var ( - ctxLogger loggerKey = struct{}{} -) - -type Logger interface { - Printf(format string, v ...any) - Println(...any) -} - -// NewLoggerContext returns a derieved context that stores the passed logger. -func NewLoggerContext(ctx context.Context, logger Logger) context.Context { - if logger == nil { - panic("logger is nil") - } - return context.WithValue(ctx, ctxLogger, logger) -} - -// LoggerFromContext returns the logger to use for the passed context. -func LoggerFromContext(ctx context.Context) Logger { - logger := ctx.Value(ctxLogger) - if logger == nil { - if testing.Testing() { - panic("accessed global logger") - } - return log.Default() - } - - return logger.(Logger) -} diff --git a/log/logging_test.go b/log/logging_test.go deleted file mode 100644 index c07e27c..0000000 --- a/log/logging_test.go +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package log - -import ( - "bytes" - "log" - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGlobalLogger(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - defer func() { - if err := recover(); assert.NotNil(err) { - assert.Equal("accessed global logger", err) - } - }() - - logger := LoggerFromContext(t.Context()) - assert.Fail("should have paniced", "got logger %+v", logger) -} - -type testLogWriter struct { - mu sync.Mutex - t testing.TB -} - -func (w *testLogWriter) Write(b []byte) (int, error) { - w.t.Helper() - if !bytes.HasSuffix(b, []byte("\n")) { - b = append(b, '\n') - } - w.mu.Lock() - defer w.mu.Unlock() - w.t.Logf("%s", string(b)) - return len(b), nil -} - -func TestLoggerContext(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - testLogger := log.New(&testLogWriter{ - t: t, - }, t.Name()+": ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) - testLogger.Printf("Hello %s!", "world") - - ctx := NewLoggerContext(t.Context(), testLogger) - logger2 := LoggerFromContext(ctx) - assert.Equal(testLogger, logger2) -} - -func TestNilLoggerContext(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - defer func() { - if err := recover(); assert.NotNil(err) { - assert.Equal("logger is nil", err) - } - }() - - ctx := NewLoggerContext(t.Context(), nil) - assert.Fail("should have paniced", "got context %+v", ctx) -} diff --git a/log/test/log_test.go b/log/test/log_test.go deleted file mode 100644 index 174bff3..0000000 --- a/log/test/log_test.go +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLoggerForTest(t *testing.T) { - t.Parallel() - - log1 := NewLoggerForTest(t) - log2 := NewLoggerForTest(t) - assert.Equal(t, log1, log2) - - log1.Printf("Test output") - log1.Println("Test output") -} diff --git a/container/lru.go b/lru.go similarity index 66% rename from container/lru.go rename to lru.go index 2c7992e..1563d01 100644 --- a/container/lru.go +++ b/lru.go @@ -19,45 +19,43 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package container +package signaling import ( "container/list" "sync" ) -type cacheEntry[T any] struct { +type cacheEntry struct { key string - value T + value interface{} } -type LruCache[T any] struct { - size int // +checklocksignore: Only written to from constructor. - mu sync.Mutex - // +checklocks:mu +type LruCache struct { + size int + mu sync.Mutex entries *list.List - // +checklocks:mu - data map[string]*list.Element + data map[string]*list.Element } -func NewLruCache[T any](size int) *LruCache[T] { - return &LruCache[T]{ +func NewLruCache(size int) *LruCache { + return &LruCache{ size: size, entries: list.New(), data: make(map[string]*list.Element), } } -func (c *LruCache[T]) Set(key string, value T) { +func (c *LruCache) Set(key string, value interface{}) { c.mu.Lock() if v, found := c.data[key]; found { c.entries.MoveToFront(v) - v.Value.(*cacheEntry[T]).value = value + v.Value.(*cacheEntry).value = value c.mu.Unlock() return } - v := c.entries.PushFront(&cacheEntry[T]{ + v := c.entries.PushFront(&cacheEntry{ key: key, value: value, }) @@ -68,21 +66,20 @@ func (c *LruCache[T]) Set(key string, value T) { c.mu.Unlock() } -func (c *LruCache[T]) Get(key string) T { +func (c *LruCache) Get(key string) interface{} { c.mu.Lock() if v, found := c.data[key]; found { c.entries.MoveToFront(v) - value := v.Value.(*cacheEntry[T]).value + value := v.Value.(*cacheEntry).value c.mu.Unlock() return value } c.mu.Unlock() - var defaultValue T - return defaultValue + return nil } -func (c *LruCache[T]) Remove(key string) { +func (c *LruCache) Remove(key string) { c.mu.Lock() if v, found := c.data[key]; found { c.removeElement(v) @@ -90,28 +87,26 @@ func (c *LruCache[T]) Remove(key string) { c.mu.Unlock() } -// +checklocks:c.mu -func (c *LruCache[T]) removeOldestLocked() { +func (c *LruCache) removeOldestLocked() { v := c.entries.Back() if v != nil { c.removeElement(v) } } -func (c *LruCache[T]) RemoveOldest() { +func (c *LruCache) RemoveOldest() { c.mu.Lock() c.removeOldestLocked() c.mu.Unlock() } -// +checklocks:c.mu -func (c *LruCache[T]) removeElement(e *list.Element) { +func (c *LruCache) removeElement(e *list.Element) { c.entries.Remove(e) - entry := e.Value.(*cacheEntry[T]) + entry := e.Value.(*cacheEntry) delete(c.data, entry.key) } -func (c *LruCache[T]) Len() int { +func (c *LruCache) Len() int { c.mu.Lock() defer c.mu.Unlock() return c.entries.Len() diff --git a/container/lru_test.go b/lru_test.go similarity index 56% rename from container/lru_test.go rename to lru_test.go index 9446e86..98fbb66 100644 --- a/container/lru_test.go +++ b/lru_test.go @@ -19,101 +19,109 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package container +package signaling import ( - "strconv" + "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestLruUnbound(t *testing.T) { - t.Parallel() assert := assert.New(t) - lru := NewLruCache[int](0) + lru := NewLruCache(0) count := 10 - for i := range count { - key := strconv.Itoa(i) + for i := 0; i < count; i++ { + key := fmt.Sprintf("%d", i) lru.Set(key, i) } assert.Equal(count, lru.Len()) - for i := range count { - key := strconv.Itoa(i) - value := lru.Get(key) - assert.Equal(i, value, "Failed for %s", key) + for i := 0; i < count; i++ { + key := fmt.Sprintf("%d", i) + if value := lru.Get(key); assert.NotNil(value, "No value found for %s", key) { + assert.EqualValues(i, value) + } } // The first key ("0") is now the oldest. lru.RemoveOldest() assert.Equal(count-1, lru.Len()) - for i := range count { - key := strconv.Itoa(i) + for i := 0; i < count; i++ { + key := fmt.Sprintf("%d", i) value := lru.Get(key) - assert.Equal(i, value, "Failed for %s", key) + if i == 0 { + assert.Nil(value, "The value for key %s should have been removed", key) + continue + } else if assert.NotNil(value, "No value found for %s", key) { + assert.EqualValues(i, value) + } } // NOTE: Key "0" no longer exists below, so make sure to not set it again. // Using the same keys will update the ordering. for i := count - 1; i >= 1; i-- { - key := strconv.Itoa(i) + key := fmt.Sprintf("%d", i) lru.Set(key, i) } assert.Equal(count-1, lru.Len()) // NOTE: The same ordering as the Set calls above. for i := count - 1; i >= 1; i-- { - key := strconv.Itoa(i) - value := lru.Get(key) - assert.Equal(i, value, "Failed for %s", key) + key := fmt.Sprintf("%d", i) + if value := lru.Get(key); assert.NotNil(value, "No value found for %s", key) { + assert.EqualValues(i, value) + } } // The last key ("9") is now the oldest. lru.RemoveOldest() assert.Equal(count-2, lru.Len()) - for i := range count { - key := strconv.Itoa(i) + for i := 0; i < count; i++ { + key := fmt.Sprintf("%d", i) value := lru.Get(key) if i == 0 || i == count-1 { - assert.Equal(0, value, "The value for key %s should have been removed", key) - } else { - assert.Equal(i, value, "Failed for %s", key) + assert.Nil(value, "The value for key %s should have been removed", key) + continue + } else if assert.NotNil(value, "No value found for %s", key) { + assert.EqualValues(i, value) } } // Remove an arbitrary key from the cache - key := strconv.Itoa(count / 2) + key := fmt.Sprintf("%d", count/2) lru.Remove(key) assert.Equal(count-3, lru.Len()) - for i := range count { - key := strconv.Itoa(i) + for i := 0; i < count; i++ { + key := fmt.Sprintf("%d", i) value := lru.Get(key) if i == 0 || i == count-1 || i == count/2 { - assert.Equal(0, value, "The value for key %s should have been removed", key) - } else { - assert.Equal(i, value, "Failed for %s", key) + assert.Nil(value, "The value for key %s should have been removed", key) + continue + } else if assert.NotNil(value, "No value found for %s", key) { + assert.EqualValues(i, value) } } } func TestLruBound(t *testing.T) { - t.Parallel() assert := assert.New(t) size := 2 - lru := NewLruCache[int](size) + lru := NewLruCache(size) count := 10 - for i := range count { - key := strconv.Itoa(i) + for i := 0; i < count; i++ { + key := fmt.Sprintf("%d", i) lru.Set(key, i) } assert.Equal(size, lru.Len()) // Only the last "size" entries have been stored. - for i := range count { - key := strconv.Itoa(i) + for i := 0; i < count; i++ { + key := fmt.Sprintf("%d", i) value := lru.Get(key) if i < count-size { - assert.Equal(0, value, "The value for key %s should have been removed", key) - } else { - assert.Equal(i, value, "Failed for %s", key) + assert.Nil(value, "The value for key %s should have been removed", key) + continue + } else if assert.NotNil(value, "No value found for %s", key) { + assert.EqualValues(i, value) } } } diff --git a/mcu_common.go b/mcu_common.go new file mode 100644 index 0000000..af22d4a --- /dev/null +++ b/mcu_common.go @@ -0,0 +1,240 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "fmt" + "log" + "sync/atomic" + "time" + + "github.com/dlintw/goconf" +) + +const ( + McuTypeJanus = "janus" + McuTypeProxy = "proxy" + + McuTypeDefault = McuTypeJanus + + defaultMaxStreamBitrate = 1024 * 1024 + defaultMaxScreenBitrate = 2048 * 1024 +) + +var ( + ErrNotConnected = fmt.Errorf("not connected") +) + +type MediaType int + +const ( + MediaTypeAudio MediaType = 1 << 0 + MediaTypeVideo MediaType = 1 << 1 + MediaTypeScreen MediaType = 1 << 2 +) + +type McuListener interface { + PublicId() string + + OnUpdateOffer(client McuClient, offer map[string]interface{}) + + OnIceCandidate(client McuClient, candidate interface{}) + OnIceCompleted(client McuClient) + + SubscriberSidUpdated(subscriber McuSubscriber) + + PublisherClosed(publisher McuPublisher) + SubscriberClosed(subscriber McuSubscriber) +} + +type McuInitiator interface { + Country() string +} + +type McuSettings interface { + MaxStreamBitrate() int32 + MaxScreenBitrate() int32 + Timeout() time.Duration + + Reload(config *goconf.ConfigFile) +} + +type mcuCommonSettings struct { + maxStreamBitrate atomic.Int32 + maxScreenBitrate atomic.Int32 + + timeout atomic.Int64 +} + +func (s *mcuCommonSettings) MaxStreamBitrate() int32 { + return s.maxStreamBitrate.Load() +} + +func (s *mcuCommonSettings) MaxScreenBitrate() int32 { + return s.maxScreenBitrate.Load() +} + +func (s *mcuCommonSettings) Timeout() time.Duration { + return time.Duration(s.timeout.Load()) +} + +func (s *mcuCommonSettings) setTimeout(timeout time.Duration) { + s.timeout.Store(int64(timeout)) +} + +func (s *mcuCommonSettings) load(config *goconf.ConfigFile) error { + maxStreamBitrate, _ := config.GetInt("mcu", "maxstreambitrate") + if maxStreamBitrate <= 0 { + maxStreamBitrate = defaultMaxStreamBitrate + } + log.Printf("Maximum bandwidth %d bits/sec per publishing stream", maxStreamBitrate) + s.maxStreamBitrate.Store(int32(maxStreamBitrate)) + + maxScreenBitrate, _ := config.GetInt("mcu", "maxscreenbitrate") + if maxScreenBitrate <= 0 { + maxScreenBitrate = defaultMaxScreenBitrate + } + log.Printf("Maximum bandwidth %d bits/sec per screensharing stream", maxScreenBitrate) + s.maxScreenBitrate.Store(int32(maxScreenBitrate)) + return nil +} + +type Mcu interface { + Start(ctx context.Context) error + Stop() + Reload(config *goconf.ConfigFile) + + SetOnConnected(func()) + SetOnDisconnected(func()) + + GetStats() interface{} + + NewPublisher(ctx context.Context, listener McuListener, id string, sid string, streamType StreamType, settings NewPublisherSettings, initiator McuInitiator) (McuPublisher, error) + NewSubscriber(ctx context.Context, listener McuListener, publisher string, streamType StreamType, initiator McuInitiator) (McuSubscriber, error) +} + +// PublisherStream contains the available properties when creating a +// remote publisher in Janus. +type PublisherStream struct { + Mid string `json:"mid"` + Mindex int `json:"mindex"` + Type string `json:"type"` + + Description string `json:"description,omitempty"` + Disabled bool `json:"disabled,omitempty"` + + // For types "audio" and "video" + Codec string `json:"codec,omitempty"` + + // For type "audio" + Stereo bool `json:"stereo,omitempty"` + Fec bool `json:"fec,omitempty"` + Dtx bool `json:"dtx,omitempty"` + + // For type "video" + Simulcast bool `json:"simulcast,omitempty"` + Svc bool `json:"svc,omitempty"` + + ProfileH264 string `json:"h264_profile,omitempty"` + ProfileVP9 string `json:"vp9_profile,omitempty"` + + ExtIdVideoOrientation int `json:"videoorient_ext_id,omitempty"` + ExtIdPlayoutDelay int `json:"playoutdelay_ext_id,omitempty"` +} + +type RemotePublisherController interface { + PublisherId() string + + StartPublishing(ctx context.Context, publisher McuRemotePublisherProperties) error + StopPublishing(ctx context.Context, publisher McuRemotePublisherProperties) error + GetStreams(ctx context.Context) ([]PublisherStream, error) +} + +type RemoteMcu interface { + NewRemotePublisher(ctx context.Context, listener McuListener, controller RemotePublisherController, streamType StreamType) (McuRemotePublisher, error) + NewRemoteSubscriber(ctx context.Context, listener McuListener, publisher McuRemotePublisher) (McuRemoteSubscriber, error) +} + +type StreamType string + +const ( + StreamTypeAudio StreamType = "audio" + StreamTypeVideo StreamType = "video" + StreamTypeScreen StreamType = "screen" +) + +func IsValidStreamType(s string) bool { + switch s { + case string(StreamTypeAudio): + fallthrough + case string(StreamTypeVideo): + fallthrough + case string(StreamTypeScreen): + return true + default: + return false + } +} + +type McuClient interface { + Id() string + Sid() string + StreamType() StreamType + MaxBitrate() int + + Close(ctx context.Context) + + SendMessage(ctx context.Context, message *MessageClientMessage, data *MessageClientMessageData, callback func(error, map[string]interface{})) +} + +type McuPublisher interface { + McuClient + + HasMedia(MediaType) bool + SetMedia(MediaType) + + GetStreams(ctx context.Context) ([]PublisherStream, error) + PublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error + UnpublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error +} + +type McuSubscriber interface { + McuClient + + Publisher() string +} + +type McuRemotePublisherProperties interface { + Port() int + RtcpPort() int +} + +type McuRemotePublisher interface { + McuClient + + McuRemotePublisherProperties +} + +type McuRemoteSubscriber interface { + McuSubscriber +} diff --git a/sfu/internal/stats_prometheus_test.go b/mcu_common_test.go similarity index 55% rename from sfu/internal/stats_prometheus_test.go rename to mcu_common_test.go index cf4dd5b..6304638 100644 --- a/sfu/internal/stats_prometheus_test.go +++ b/mcu_common_test.go @@ -19,15 +19,52 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package internal +package signaling import ( "testing" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics/test" ) func TestCommonMcuStats(t *testing.T) { - t.Parallel() - test.CollectAndLint(t, commonMcuStats...) + collectAndLint(t, commonMcuStats...) +} + +type MockMcuListener struct { + publicId string +} + +func (m *MockMcuListener) PublicId() string { + return m.publicId +} + +func (m *MockMcuListener) OnUpdateOffer(client McuClient, offer map[string]interface{}) { + +} + +func (m *MockMcuListener) OnIceCandidate(client McuClient, candidate interface{}) { + +} + +func (m *MockMcuListener) OnIceCompleted(client McuClient) { + +} + +func (m *MockMcuListener) SubscriberSidUpdated(subscriber McuSubscriber) { + +} + +func (m *MockMcuListener) PublisherClosed(publisher McuPublisher) { + +} + +func (m *MockMcuListener) SubscriberClosed(subscriber McuSubscriber) { + +} + +type MockMcuInitiator struct { + country string +} + +func (m *MockMcuInitiator) Country() string { + return m.country } diff --git a/mcu_janus.go b/mcu_janus.go new file mode 100644 index 0000000..1a68003 --- /dev/null +++ b/mcu_janus.go @@ -0,0 +1,896 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/dlintw/goconf" + "github.com/notedit/janus-go" +) + +const ( + pluginVideoRoom = "janus.plugin.videoroom" + + keepaliveInterval = 30 * time.Second + + videoPublisherUserId = 1 + screenPublisherUserId = 2 + + initialReconnectInterval = 1 * time.Second + maxReconnectInterval = 32 * time.Second +) + +var ( + ErrRemoteStreamsNotSupported = errors.New("Need Janus 1.1.0 for remote streams") + + streamTypeUserIds = map[StreamType]uint64{ + StreamTypeVideo: videoPublisherUserId, + StreamTypeScreen: screenPublisherUserId, + } +) + +func getStreamId(publisherId string, streamType StreamType) string { + return fmt.Sprintf("%s|%s", publisherId, streamType) +} + +func getPluginValue(data janus.PluginData, pluginName string, key string) interface{} { + if data.Plugin != pluginName { + return nil + } + + return data.Data[key] +} + +func convertIntValue(value interface{}) (uint64, error) { + switch t := value.(type) { + case float64: + if t < 0 { + return 0, fmt.Errorf("Unsupported float64 number: %+v", t) + } + return uint64(t), nil + case uint64: + return t, nil + case int: + if t < 0 { + return 0, fmt.Errorf("Unsupported int number: %+v", t) + } + return uint64(t), nil + case int64: + if t < 0 { + return 0, fmt.Errorf("Unsupported int64 number: %+v", t) + } + return uint64(t), nil + case json.Number: + r, err := t.Int64() + if err != nil { + return 0, err + } else if r < 0 { + return 0, fmt.Errorf("Unsupported JSON number: %+v", t) + } + return uint64(r), nil + default: + return 0, fmt.Errorf("Unknown number type: %+v (%T)", t, t) + } +} + +func getPluginIntValue(data janus.PluginData, pluginName string, key string) uint64 { + val := getPluginValue(data, pluginName, key) + if val == nil { + return 0 + } + + result, err := convertIntValue(val) + if err != nil { + log.Printf("Invalid value %+v for %s: %s", val, key, err) + result = 0 + } + return result +} + +func getPluginStringValue(data janus.PluginData, pluginName string, key string) string { + val := getPluginValue(data, pluginName, key) + if val == nil { + return "" + } + + strVal, ok := val.(string) + if !ok { + return "" + } + + return strVal +} + +// TODO(jojo): Lots of error handling still missing. + +type clientInterface interface { + NotifyReconnected() +} + +type mcuJanusSettings struct { + mcuCommonSettings +} + +func newMcuJanusSettings(config *goconf.ConfigFile) (McuSettings, error) { + settings := &mcuJanusSettings{} + if err := settings.load(config); err != nil { + return nil, err + } + + return settings, nil +} + +func (s *mcuJanusSettings) load(config *goconf.ConfigFile) error { + if err := s.mcuCommonSettings.load(config); err != nil { + return err + } + + mcuTimeoutSeconds, _ := config.GetInt("mcu", "timeout") + if mcuTimeoutSeconds <= 0 { + mcuTimeoutSeconds = defaultMcuTimeoutSeconds + } + mcuTimeout := time.Duration(mcuTimeoutSeconds) * time.Second + log.Printf("Using a timeout of %s for MCU requests", mcuTimeout) + s.setTimeout(mcuTimeout) + return nil +} + +func (s *mcuJanusSettings) Reload(config *goconf.ConfigFile) { + if err := s.load(config); err != nil { + log.Printf("Error reloading MCU settings: %s", err) + } +} + +type mcuJanus struct { + url string + mu sync.Mutex + + settings McuSettings + + createJanusGateway func(ctx context.Context, wsURL string, listener GatewayListener) (JanusGatewayInterface, error) + + gw JanusGatewayInterface + session *JanusSession + handle *JanusHandle + + version int + + closeChan chan struct{} + + muClients sync.Mutex + clients map[clientInterface]bool + clientId atomic.Uint64 + + publishers map[string]*mcuJanusPublisher + publisherCreated Notifier + publisherConnected Notifier + remotePublishers map[string]*mcuJanusRemotePublisher + + reconnectTimer *time.Timer + reconnectInterval time.Duration + + connectedSince time.Time + onConnected atomic.Value + onDisconnected atomic.Value +} + +func emptyOnConnected() {} +func emptyOnDisconnected() {} + +func NewMcuJanus(ctx context.Context, url string, config *goconf.ConfigFile) (Mcu, error) { + settings, err := newMcuJanusSettings(config) + if err != nil { + return nil, err + } + + mcu := &mcuJanus{ + url: url, + settings: settings, + closeChan: make(chan struct{}, 1), + clients: make(map[clientInterface]bool), + + publishers: make(map[string]*mcuJanusPublisher), + remotePublishers: make(map[string]*mcuJanusRemotePublisher), + + createJanusGateway: func(ctx context.Context, wsURL string, listener GatewayListener) (JanusGatewayInterface, error) { + return NewJanusGateway(ctx, wsURL, listener) + }, + reconnectInterval: initialReconnectInterval, + } + mcu.onConnected.Store(emptyOnConnected) + mcu.onDisconnected.Store(emptyOnDisconnected) + + mcu.reconnectTimer = time.AfterFunc(mcu.reconnectInterval, func() { + mcu.doReconnect(context.Background()) + }) + mcu.reconnectTimer.Stop() + if mcu.url != "" { + if err := mcu.reconnect(ctx); err != nil { + return nil, err + } + } + return mcu, nil +} + +func (m *mcuJanus) disconnect() { + if handle := m.handle; handle != nil { + m.handle = nil + m.closeChan <- struct{}{} + if _, err := handle.Detach(context.TODO()); err != nil { + log.Printf("Error detaching handle %d: %s", handle.Id, err) + } + } + if m.session != nil { + if _, err := m.session.Destroy(context.TODO()); err != nil { + log.Printf("Error destroying session %d: %s", m.session.Id, err) + } + m.session = nil + } + if m.gw != nil { + if err := m.gw.Close(); err != nil { + log.Println("Error while closing connection to MCU", err) + } + m.gw = nil + } +} + +func (m *mcuJanus) reconnect(ctx context.Context) error { + m.disconnect() + gw, err := m.createJanusGateway(ctx, m.url, m) + if err != nil { + return err + } + + m.gw = gw + m.reconnectTimer.Stop() + return nil +} + +func (m *mcuJanus) doReconnect(ctx context.Context) { + if err := m.reconnect(ctx); err != nil { + m.scheduleReconnect(err) + return + } + if err := m.Start(ctx); err != nil { + m.scheduleReconnect(err) + return + } + + log.Println("Reconnection to Janus gateway successful") + m.mu.Lock() + clear(m.publishers) + m.publisherCreated.Reset() + m.publisherConnected.Reset() + m.reconnectInterval = initialReconnectInterval + m.mu.Unlock() + + m.muClients.Lock() + for client := range m.clients { + go client.NotifyReconnected() + } + m.muClients.Unlock() +} + +func (m *mcuJanus) scheduleReconnect(err error) { + m.mu.Lock() + defer m.mu.Unlock() + m.reconnectTimer.Reset(m.reconnectInterval) + if err == nil { + log.Printf("Connection to Janus gateway was interrupted, reconnecting in %s", m.reconnectInterval) + } else { + log.Printf("Reconnect to Janus gateway failed (%s), reconnecting in %s", err, m.reconnectInterval) + } + + m.reconnectInterval = m.reconnectInterval * 2 + if m.reconnectInterval > maxReconnectInterval { + m.reconnectInterval = maxReconnectInterval + } +} + +func (m *mcuJanus) ConnectionInterrupted() { + m.scheduleReconnect(nil) + m.notifyOnDisconnected() +} + +func (m *mcuJanus) isMultistream() bool { + return m.version >= 1000 +} + +func (m *mcuJanus) hasRemotePublisher() bool { + return m.version >= 1100 +} + +func (m *mcuJanus) Start(ctx context.Context) error { + if m.url == "" { + if err := m.reconnect(ctx); err != nil { + return err + } + } + info, err := m.gw.Info(ctx) + if err != nil { + return err + } + + log.Printf("Connected to %s %s by %s", info.Name, info.VersionString, info.Author) + plugin, found := info.Plugins[pluginVideoRoom] + if !found { + return fmt.Errorf("Plugin %s is not supported", pluginVideoRoom) + } + + m.version = info.Version + + log.Printf("Found %s %s by %s", plugin.Name, plugin.VersionString, plugin.Author) + if !info.DataChannels { + return fmt.Errorf("Data channels are not supported") + } + + log.Println("Data channels are supported") + if !info.FullTrickle { + log.Println("WARNING: Full-Trickle is NOT enabled in Janus!") + } else { + log.Println("Full-Trickle is enabled") + } + + if m.session, err = m.gw.Create(ctx); err != nil { + m.disconnect() + return err + } + log.Println("Created Janus session", m.session.Id) + m.connectedSince = time.Now() + + if m.handle, err = m.session.Attach(ctx, pluginVideoRoom); err != nil { + m.disconnect() + return err + } + log.Println("Created Janus handle", m.handle.Id) + + go m.run() + + m.notifyOnConnected() + return nil +} + +func (m *mcuJanus) registerClient(client clientInterface) { + m.muClients.Lock() + m.clients[client] = true + m.muClients.Unlock() +} + +func (m *mcuJanus) unregisterClient(client clientInterface) { + m.muClients.Lock() + delete(m.clients, client) + m.muClients.Unlock() +} + +func (m *mcuJanus) run() { + ticker := time.NewTicker(keepaliveInterval) + defer ticker.Stop() + +loop: + for { + select { + case <-ticker.C: + m.sendKeepalive(context.Background()) + case <-m.closeChan: + break loop + } + } +} + +func (m *mcuJanus) Stop() { + m.disconnect() + m.reconnectTimer.Stop() +} + +func (m *mcuJanus) Reload(config *goconf.ConfigFile) { + m.settings.Reload(config) +} + +func (m *mcuJanus) SetOnConnected(f func()) { + if f == nil { + f = emptyOnConnected + } + + m.onConnected.Store(f) +} + +func (m *mcuJanus) notifyOnConnected() { + f := m.onConnected.Load().(func()) + f() +} + +func (m *mcuJanus) SetOnDisconnected(f func()) { + if f == nil { + f = emptyOnDisconnected + } + + m.onDisconnected.Store(f) +} + +func (m *mcuJanus) notifyOnDisconnected() { + f := m.onDisconnected.Load().(func()) + f() +} + +type mcuJanusConnectionStats struct { + Url string `json:"url"` + Connected bool `json:"connected"` + Publishers int64 `json:"publishers"` + Clients int64 `json:"clients"` + Uptime *time.Time `json:"uptime,omitempty"` +} + +func (m *mcuJanus) GetStats() interface{} { + result := mcuJanusConnectionStats{ + Url: m.url, + } + if m.session != nil { + result.Connected = true + result.Uptime = &m.connectedSince + } + m.mu.Lock() + result.Publishers = int64(len(m.publishers)) + m.mu.Unlock() + m.muClients.Lock() + result.Clients = int64(len(m.clients)) + m.muClients.Unlock() + return result +} + +func (m *mcuJanus) sendKeepalive(ctx context.Context) { + if _, err := m.session.KeepAlive(ctx); err != nil { + log.Println("Could not send keepalive request", err) + if e, ok := err.(*janus.ErrorMsg); ok { + switch e.Err.Code { + case JANUS_ERROR_SESSION_NOT_FOUND: + m.scheduleReconnect(err) + } + } + } +} + +func (m *mcuJanus) SubscriberConnected(id string, publisher string, streamType StreamType) { + m.mu.Lock() + defer m.mu.Unlock() + + if p, found := m.publishers[getStreamId(publisher, streamType)]; found { + p.stats.AddSubscriber(id) + } +} + +func (m *mcuJanus) SubscriberDisconnected(id string, publisher string, streamType StreamType) { + m.mu.Lock() + defer m.mu.Unlock() + + if p, found := m.publishers[getStreamId(publisher, streamType)]; found { + p.stats.RemoveSubscriber(id) + } +} + +func (m *mcuJanus) createPublisherRoom(ctx context.Context, handle *JanusHandle, id string, streamType StreamType, settings NewPublisherSettings) (uint64, int, error) { + create_msg := map[string]interface{}{ + "request": "create", + "description": getStreamId(id, streamType), + // We publish every stream in its own Janus room. + "publishers": 1, + // Do not use the video-orientation RTP extension as it breaks video + // orientation changes in Firefox. + "videoorient_ext": false, + } + if codec := settings.AudioCodec; codec != "" { + create_msg["audiocodec"] = codec + } + if codec := settings.VideoCodec; codec != "" { + create_msg["videocodec"] = codec + } + if profile := settings.VP9Profile; profile != "" { + create_msg["vp9_profile"] = profile + } + if profile := settings.H264Profile; profile != "" { + create_msg["h264_profile"] = profile + } + var maxBitrate int + if streamType == StreamTypeScreen { + maxBitrate = int(m.settings.MaxScreenBitrate()) + } else { + maxBitrate = int(m.settings.MaxStreamBitrate()) + } + bitrate := settings.Bitrate + if bitrate <= 0 { + bitrate = maxBitrate + } else { + bitrate = min(bitrate, maxBitrate) + } + create_msg["bitrate"] = bitrate + create_response, err := handle.Request(ctx, create_msg) + if err != nil { + if _, err2 := handle.Detach(ctx); err2 != nil { + log.Printf("Error detaching handle %d: %s", handle.Id, err2) + } + return 0, 0, err + } + + roomId := getPluginIntValue(create_response.PluginData, pluginVideoRoom, "room") + if roomId == 0 { + if _, err := handle.Detach(ctx); err != nil { + log.Printf("Error detaching handle %d: %s", handle.Id, err) + } + return 0, 0, fmt.Errorf("No room id received: %+v", create_response) + } + + log.Println("Created room", roomId, create_response.PluginData) + return roomId, bitrate, nil +} + +func (m *mcuJanus) getOrCreatePublisherHandle(ctx context.Context, id string, streamType StreamType, settings NewPublisherSettings) (*JanusHandle, uint64, uint64, int, error) { + session := m.session + if session == nil { + return nil, 0, 0, 0, ErrNotConnected + } + handle, err := session.Attach(ctx, pluginVideoRoom) + if err != nil { + return nil, 0, 0, 0, err + } + + log.Printf("Attached %s as publisher %d to plugin %s in session %d", streamType, handle.Id, pluginVideoRoom, session.Id) + + roomId, bitrate, err := m.createPublisherRoom(ctx, handle, id, streamType, settings) + if err != nil { + if _, err2 := handle.Detach(ctx); err2 != nil { + log.Printf("Error detaching handle %d: %s", handle.Id, err2) + } + return nil, 0, 0, 0, err + } + + msg := map[string]interface{}{ + "request": "join", + "ptype": "publisher", + "room": roomId, + "id": streamTypeUserIds[streamType], + } + + response, err := handle.Message(ctx, msg, nil) + if err != nil { + if _, err2 := handle.Detach(ctx); err2 != nil { + log.Printf("Error detaching handle %d: %s", handle.Id, err2) + } + return nil, 0, 0, 0, err + } + + return handle, response.Session, roomId, bitrate, nil +} + +func (m *mcuJanus) NewPublisher(ctx context.Context, listener McuListener, id string, sid string, streamType StreamType, settings NewPublisherSettings, initiator McuInitiator) (McuPublisher, error) { + if _, found := streamTypeUserIds[streamType]; !found { + return nil, fmt.Errorf("Unsupported stream type %s", streamType) + } + + handle, session, roomId, maxBitrate, err := m.getOrCreatePublisherHandle(ctx, id, streamType, settings) + if err != nil { + return nil, err + } + + client := &mcuJanusPublisher{ + mcuJanusClient: mcuJanusClient{ + mcu: m, + listener: listener, + + id: m.clientId.Add(1), + session: session, + roomId: roomId, + sid: sid, + streamType: streamType, + maxBitrate: maxBitrate, + + handle: handle, + handleId: handle.Id, + closeChan: make(chan struct{}, 1), + deferred: make(chan func(), 64), + }, + sdpReady: NewCloser(), + id: id, + settings: settings, + } + client.mcuJanusClient.handleEvent = client.handleEvent + client.mcuJanusClient.handleHangup = client.handleHangup + client.mcuJanusClient.handleDetached = client.handleDetached + client.mcuJanusClient.handleConnected = client.handleConnected + client.mcuJanusClient.handleSlowLink = client.handleSlowLink + client.mcuJanusClient.handleMedia = client.handleMedia + + m.registerClient(client) + log.Printf("Publisher %s is using handle %d", client.id, client.handleId) + go client.run(handle, client.closeChan) + m.mu.Lock() + m.publishers[getStreamId(id, streamType)] = client + m.publisherCreated.Notify(getStreamId(id, streamType)) + m.mu.Unlock() + statsPublishersCurrent.WithLabelValues(string(streamType)).Inc() + statsPublishersTotal.WithLabelValues(string(streamType)).Inc() + return client, nil +} + +func (m *mcuJanus) getPublisher(ctx context.Context, publisher string, streamType StreamType) (*mcuJanusPublisher, error) { + // Do the direct check immediately as this should be the normal case. + key := getStreamId(publisher, streamType) + m.mu.Lock() + if result, found := m.publishers[key]; found { + m.mu.Unlock() + return result, nil + } + + waiter := m.publisherCreated.NewWaiter(key) + m.mu.Unlock() + defer m.publisherCreated.Release(waiter) + + for { + m.mu.Lock() + result := m.publishers[key] + m.mu.Unlock() + if result != nil { + return result, nil + } + + if err := waiter.Wait(ctx); err != nil { + return nil, err + } + } +} + +func (m *mcuJanus) getOrCreateSubscriberHandle(ctx context.Context, publisher string, streamType StreamType) (*JanusHandle, *mcuJanusPublisher, error) { + var pub *mcuJanusPublisher + var err error + if pub, err = m.getPublisher(ctx, publisher, streamType); err != nil { + return nil, nil, err + } + + session := m.session + if session == nil { + return nil, nil, ErrNotConnected + } + + handle, err := session.Attach(ctx, pluginVideoRoom) + if err != nil { + return nil, nil, err + } + + log.Printf("Attached subscriber to room %d of publisher %s in plugin %s in session %d as %d", pub.roomId, publisher, pluginVideoRoom, session.Id, handle.Id) + return handle, pub, nil +} + +func (m *mcuJanus) NewSubscriber(ctx context.Context, listener McuListener, publisher string, streamType StreamType, initiator McuInitiator) (McuSubscriber, error) { + if _, found := streamTypeUserIds[streamType]; !found { + return nil, fmt.Errorf("Unsupported stream type %s", streamType) + } + + handle, pub, err := m.getOrCreateSubscriberHandle(ctx, publisher, streamType) + if err != nil { + return nil, err + } + + client := &mcuJanusSubscriber{ + mcuJanusClient: mcuJanusClient{ + mcu: m, + listener: listener, + + id: m.clientId.Add(1), + roomId: pub.roomId, + sid: strconv.FormatUint(handle.Id, 10), + streamType: streamType, + maxBitrate: pub.MaxBitrate(), + + handle: handle, + handleId: handle.Id, + closeChan: make(chan struct{}, 1), + deferred: make(chan func(), 64), + }, + publisher: publisher, + } + client.mcuJanusClient.handleEvent = client.handleEvent + client.mcuJanusClient.handleHangup = client.handleHangup + client.mcuJanusClient.handleDetached = client.handleDetached + client.mcuJanusClient.handleConnected = client.handleConnected + client.mcuJanusClient.handleSlowLink = client.handleSlowLink + client.mcuJanusClient.handleMedia = client.handleMedia + m.registerClient(client) + go client.run(handle, client.closeChan) + statsSubscribersCurrent.WithLabelValues(string(streamType)).Inc() + statsSubscribersTotal.WithLabelValues(string(streamType)).Inc() + return client, nil +} + +func (m *mcuJanus) getOrCreateRemotePublisher(ctx context.Context, controller RemotePublisherController, streamType StreamType, settings NewPublisherSettings) (*mcuJanusRemotePublisher, error) { + m.mu.Lock() + defer m.mu.Unlock() + pub, found := m.remotePublishers[getStreamId(controller.PublisherId(), streamType)] + if found { + return pub, nil + } + + streams, err := controller.GetStreams(ctx) + if err != nil { + return nil, err + } + + if len(streams) == 0 { + return nil, errors.New("remote publisher has no streams") + } + + session := m.session + if session == nil { + return nil, ErrNotConnected + } + + handle, err := session.Attach(ctx, pluginVideoRoom) + if err != nil { + return nil, err + } + + roomId, maxBitrate, err := m.createPublisherRoom(ctx, handle, controller.PublisherId(), streamType, settings) + if err != nil { + if _, err2 := handle.Detach(ctx); err2 != nil { + log.Printf("Error detaching handle %d: %s", handle.Id, err2) + } + return nil, err + } + + response, err := handle.Request(ctx, map[string]interface{}{ + "request": "add_remote_publisher", + "room": roomId, + "id": streamTypeUserIds[streamType], + "streams": streams, + }) + if err != nil { + if _, err2 := handle.Detach(ctx); err2 != nil { + log.Printf("Error detaching handle %d: %s", handle.Id, err2) + } + return nil, err + } + + id := getPluginIntValue(response.PluginData, pluginVideoRoom, "id") + port := getPluginIntValue(response.PluginData, pluginVideoRoom, "port") + rtcp_port := getPluginIntValue(response.PluginData, pluginVideoRoom, "rtcp_port") + + pub = &mcuJanusRemotePublisher{ + mcuJanusPublisher: mcuJanusPublisher{ + mcuJanusClient: mcuJanusClient{ + mcu: m, + + id: id, + session: response.Session, + roomId: roomId, + sid: strconv.FormatUint(handle.Id, 10), + streamType: streamType, + maxBitrate: maxBitrate, + + handle: handle, + handleId: handle.Id, + closeChan: make(chan struct{}, 1), + deferred: make(chan func(), 64), + }, + + sdpReady: NewCloser(), + id: controller.PublisherId(), + settings: settings, + }, + + controller: controller, + + port: int(port), + rtcpPort: int(rtcp_port), + } + pub.mcuJanusClient.handleEvent = pub.handleEvent + pub.mcuJanusClient.handleHangup = pub.handleHangup + pub.mcuJanusClient.handleDetached = pub.handleDetached + pub.mcuJanusClient.handleConnected = pub.handleConnected + pub.mcuJanusClient.handleSlowLink = pub.handleSlowLink + pub.mcuJanusClient.handleMedia = pub.handleMedia + + if err := controller.StartPublishing(ctx, pub); err != nil { + go pub.Close(context.Background()) + return nil, err + } + + m.remotePublishers[getStreamId(controller.PublisherId(), streamType)] = pub + + return pub, nil +} + +func (m *mcuJanus) NewRemotePublisher(ctx context.Context, listener McuListener, controller RemotePublisherController, streamType StreamType) (McuRemotePublisher, error) { + if _, found := streamTypeUserIds[streamType]; !found { + return nil, fmt.Errorf("Unsupported stream type %s", streamType) + } + + if !m.hasRemotePublisher() { + return nil, ErrRemoteStreamsNotSupported + } + + pub, err := m.getOrCreateRemotePublisher(ctx, controller, streamType, NewPublisherSettings{}) + if err != nil { + return nil, err + } + + pub.addRef() + return pub, nil +} + +func (m *mcuJanus) NewRemoteSubscriber(ctx context.Context, listener McuListener, publisher McuRemotePublisher) (McuRemoteSubscriber, error) { + pub, ok := publisher.(*mcuJanusRemotePublisher) + if !ok { + return nil, errors.New("unsupported remote publisher") + } + + session := m.session + if session == nil { + return nil, ErrNotConnected + } + + handle, err := session.Attach(ctx, pluginVideoRoom) + if err != nil { + return nil, err + } + + log.Printf("Attached subscriber to room %d of publisher %s in plugin %s in session %d as %d", pub.roomId, pub.id, pluginVideoRoom, session.Id, handle.Id) + + client := &mcuJanusRemoteSubscriber{ + mcuJanusSubscriber: mcuJanusSubscriber{ + mcuJanusClient: mcuJanusClient{ + mcu: m, + listener: listener, + + id: m.clientId.Add(1), + roomId: pub.roomId, + sid: strconv.FormatUint(handle.Id, 10), + streamType: publisher.StreamType(), + maxBitrate: pub.MaxBitrate(), + + handle: handle, + handleId: handle.Id, + closeChan: make(chan struct{}, 1), + deferred: make(chan func(), 64), + }, + publisher: pub.id, + }, + } + client.remote.Store(pub) + pub.addRef() + client.mcuJanusClient.handleEvent = client.handleEvent + client.mcuJanusClient.handleHangup = client.handleHangup + client.mcuJanusClient.handleDetached = client.handleDetached + client.mcuJanusClient.handleConnected = client.handleConnected + client.mcuJanusClient.handleSlowLink = client.handleSlowLink + client.mcuJanusClient.handleMedia = client.handleMedia + m.registerClient(client) + go client.run(handle, client.closeChan) + statsSubscribersCurrent.WithLabelValues(string(publisher.StreamType())).Inc() + statsSubscribersTotal.WithLabelValues(string(publisher.StreamType())).Inc() + return client, nil +} diff --git a/mcu_janus_client.go b/mcu_janus_client.go new file mode 100644 index 0000000..f1d254b --- /dev/null +++ b/mcu_janus_client.go @@ -0,0 +1,216 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "log" + "reflect" + "strconv" + "sync" + + "github.com/notedit/janus-go" +) + +type mcuJanusClient struct { + mcu *mcuJanus + listener McuListener + mu sync.Mutex // nolint + + id uint64 + session uint64 + roomId uint64 + sid string + streamType StreamType + maxBitrate int + + handle *JanusHandle + handleId uint64 + closeChan chan struct{} + deferred chan func() + + handleEvent func(event *janus.EventMsg) + handleHangup func(event *janus.HangupMsg) + handleDetached func(event *janus.DetachedMsg) + handleConnected func(event *janus.WebRTCUpMsg) + handleSlowLink func(event *janus.SlowLinkMsg) + handleMedia func(event *janus.MediaMsg) +} + +func (c *mcuJanusClient) Id() string { + return strconv.FormatUint(c.id, 10) +} + +func (c *mcuJanusClient) Sid() string { + return c.sid +} + +func (c *mcuJanusClient) StreamType() StreamType { + return c.streamType +} + +func (c *mcuJanusClient) MaxBitrate() int { + return c.maxBitrate +} + +func (c *mcuJanusClient) Close(ctx context.Context) { +} + +func (c *mcuJanusClient) SendMessage(ctx context.Context, message *MessageClientMessage, data *MessageClientMessageData, callback func(error, map[string]interface{})) { +} + +func (c *mcuJanusClient) closeClient(ctx context.Context) bool { + if handle := c.handle; handle != nil { + c.handle = nil + close(c.closeChan) + if _, err := handle.Detach(ctx); err != nil { + if e, ok := err.(*janus.ErrorMsg); !ok || e.Err.Code != JANUS_ERROR_HANDLE_NOT_FOUND { + log.Println("Could not detach client", handle.Id, err) + } + } + return true + } + + return false +} + +func (c *mcuJanusClient) run(handle *JanusHandle, closeChan <-chan struct{}) { +loop: + for { + select { + case msg := <-handle.Events: + switch t := msg.(type) { + case *janus.EventMsg: + c.handleEvent(t) + case *janus.HangupMsg: + c.handleHangup(t) + case *janus.DetachedMsg: + c.handleDetached(t) + case *janus.MediaMsg: + c.handleMedia(t) + case *janus.WebRTCUpMsg: + c.handleConnected(t) + case *janus.SlowLinkMsg: + c.handleSlowLink(t) + case *TrickleMsg: + c.handleTrickle(t) + default: + log.Println("Received unsupported event type", msg, reflect.TypeOf(msg)) + } + case f := <-c.deferred: + f() + case <-closeChan: + break loop + } + } +} + +func (c *mcuJanusClient) sendOffer(ctx context.Context, offer map[string]interface{}, callback func(error, map[string]interface{})) { + handle := c.handle + if handle == nil { + callback(ErrNotConnected, nil) + return + } + + configure_msg := map[string]interface{}{ + "request": "configure", + "audio": true, + "video": true, + "data": true, + } + answer_msg, err := handle.Message(ctx, configure_msg, offer) + if err != nil { + callback(err, nil) + return + } + + callback(nil, answer_msg.Jsep) +} + +func (c *mcuJanusClient) sendAnswer(ctx context.Context, answer map[string]interface{}, callback func(error, map[string]interface{})) { + handle := c.handle + if handle == nil { + callback(ErrNotConnected, nil) + return + } + + start_msg := map[string]interface{}{ + "request": "start", + "room": c.roomId, + } + start_response, err := handle.Message(ctx, start_msg, answer) + if err != nil { + callback(err, nil) + return + } + log.Println("Started listener", start_response) + callback(nil, nil) +} + +func (c *mcuJanusClient) sendCandidate(ctx context.Context, candidate interface{}, callback func(error, map[string]interface{})) { + handle := c.handle + if handle == nil { + callback(ErrNotConnected, nil) + return + } + + if _, err := handle.Trickle(ctx, candidate); err != nil { + callback(err, nil) + return + } + callback(nil, nil) +} + +func (c *mcuJanusClient) handleTrickle(event *TrickleMsg) { + if event.Candidate.Completed { + c.listener.OnIceCompleted(c) + } else { + c.listener.OnIceCandidate(c, event.Candidate) + } +} + +func (c *mcuJanusClient) selectStream(ctx context.Context, stream *streamSelection, callback func(error, map[string]interface{})) { + handle := c.handle + if handle == nil { + callback(ErrNotConnected, nil) + return + } + + if stream == nil || !stream.HasValues() { + callback(nil, nil) + return + } + + configure_msg := map[string]interface{}{ + "request": "configure", + } + if stream != nil { + stream.AddToMessage(configure_msg) + } + _, err := handle.Message(ctx, configure_msg, nil) + if err != nil { + callback(err, nil) + return + } + + callback(nil, nil) +} diff --git a/sfu/janus/publisher.go b/mcu_janus_publisher.go similarity index 57% rename from sfu/janus/publisher.go rename to mcu_janus_publisher.go index 3a2a511..9e82d80 100644 --- a/sfu/janus/publisher.go +++ b/mcu_janus_publisher.go @@ -19,23 +19,19 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package janus +package signaling import ( "context" "errors" "fmt" + "log" "strconv" "strings" "sync/atomic" + "github.com/notedit/janus-go" "github.com/pion/sdp/v3" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - sfuinternal "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" ) const ( @@ -48,65 +44,61 @@ const ( sdpHasAnswer = 2 ) -type janusPublisher struct { - janusClient +type mcuJanusPublisher struct { + mcuJanusClient - id api.PublicSessionId - settings sfu.NewPublisherSettings + id string + settings NewPublisherSettings stats publisherStatsCounter - sdpFlags internal.Flags - sdpReady *internal.Closer + sdpFlags Flags + sdpReady *Closer offerSdp atomic.Pointer[sdp.SessionDescription] answerSdp atomic.Pointer[sdp.SessionDescription] } -func (p *janusPublisher) PublisherId() api.PublicSessionId { - return p.id -} - -func (p *janusPublisher) handleEvent(event *janus.EventMsg) { +func (p *mcuJanusPublisher) handleEvent(event *janus.EventMsg) { if videoroom := getPluginStringValue(event.Plugindata, pluginVideoRoom, "videoroom"); videoroom != "" { ctx := context.TODO() switch videoroom { case "destroyed": - p.logger.Printf("Publisher %d: associated room has been destroyed, closing", p.handleId.Load()) + log.Printf("Publisher %d: associated room has been destroyed, closing", p.handleId) go p.Close(ctx) case "slow_link": // Ignore, processed through "handleSlowLink" in the general events. default: - p.logger.Printf("Unsupported videoroom publisher event in %d: %+v", p.handleId.Load(), event) + log.Printf("Unsupported videoroom publisher event in %d: %+v", p.handleId, event) } } else { - p.logger.Printf("Unsupported publisher event in %d: %+v", p.handleId.Load(), event) + log.Printf("Unsupported publisher event in %d: %+v", p.handleId, event) } } -func (p *janusPublisher) handleHangup(event *janus.HangupMsg) { - p.logger.Printf("Publisher %d received hangup (%s), closing", p.handleId.Load(), event.Reason) +func (p *mcuJanusPublisher) handleHangup(event *janus.HangupMsg) { + log.Printf("Publisher %d received hangup (%s), closing", p.handleId, event.Reason) go p.Close(context.Background()) } -func (p *janusPublisher) handleDetached(event *janus.DetachedMsg) { - p.logger.Printf("Publisher %d received detached, closing", p.handleId.Load()) +func (p *mcuJanusPublisher) handleDetached(event *janus.DetachedMsg) { + log.Printf("Publisher %d received detached, closing", p.handleId) go p.Close(context.Background()) } -func (p *janusPublisher) handleConnected(event *janus.WebRTCUpMsg) { - p.logger.Printf("Publisher %d received connected", p.handleId.Load()) - p.mcu.notifyPublisherConnected(p.id, p.streamType) +func (p *mcuJanusPublisher) handleConnected(event *janus.WebRTCUpMsg) { + log.Printf("Publisher %d received connected", p.handleId) + p.mcu.publisherConnected.Notify(getStreamId(p.id, p.streamType)) } -func (p *janusPublisher) handleSlowLink(event *janus.SlowLinkMsg) { +func (p *mcuJanusPublisher) handleSlowLink(event *janus.SlowLinkMsg) { if event.Uplink { - p.logger.Printf("Publisher %s (%d) is reporting %d lost packets on the uplink (Janus -> client)", p.listener.PublicId(), p.handleId.Load(), event.Lost) + log.Printf("Publisher %s (%d) is reporting %d lost packets on the uplink (Janus -> client)", p.listener.PublicId(), p.handleId, event.Lost) } else { - p.logger.Printf("Publisher %s (%d) is reporting %d lost packets on the downlink (client -> Janus)", p.listener.PublicId(), p.handleId.Load(), event.Lost) + log.Printf("Publisher %s (%d) is reporting %d lost packets on the downlink (client -> Janus)", p.listener.PublicId(), p.handleId, event.Lost) } } -func (p *janusPublisher) handleMedia(event *janus.MediaMsg) { - mediaType := sfu.StreamType(event.Type) - if mediaType == sfu.StreamTypeVideo && p.streamType == sfu.StreamTypeScreen { +func (p *mcuJanusPublisher) handleMedia(event *janus.MediaMsg) { + mediaType := StreamType(event.Type) + if mediaType == StreamTypeVideo && p.streamType == StreamTypeScreen { // We want to differentiate between audio, video and screensharing mediaType = p.streamType } @@ -114,50 +106,46 @@ func (p *janusPublisher) handleMedia(event *janus.MediaMsg) { p.stats.EnableStream(mediaType, event.Receiving) } -func (p *janusPublisher) HasMedia(mt sfu.MediaType) bool { +func (p *mcuJanusPublisher) HasMedia(mt MediaType) bool { return (p.settings.MediaTypes & mt) == mt } -func (p *janusPublisher) SetMedia(mt sfu.MediaType) { +func (p *mcuJanusPublisher) SetMedia(mt MediaType) { p.settings.MediaTypes = mt } -func (p *janusPublisher) NotifyReconnected() { +func (p *mcuJanusPublisher) NotifyReconnected() { ctx := context.TODO() handle, session, roomId, _, err := p.mcu.getOrCreatePublisherHandle(ctx, p.id, p.streamType, p.settings) if err != nil { - p.logger.Printf("Could not reconnect publisher %s: %s", p.id, err) + log.Printf("Could not reconnect publisher %s: %s", p.id, err) // TODO(jojo): Retry return } - if prev := p.handle.Swap(handle); prev != nil { - if _, err := prev.Detach(context.Background()); err != nil { - p.logger.Printf("Error detaching old publisher handle %d: %s", prev.Id, err) - } - } - p.handleId.Store(handle.Id) + p.handle = handle + p.handleId = handle.Id p.session = session p.roomId = roomId - p.logger.Printf("Publisher %s reconnected on handle %d", p.id, p.handleId.Load()) + log.Printf("Publisher %s reconnected on handle %d", p.id, p.handleId) } -func (p *janusPublisher) Close(ctx context.Context) { +func (p *mcuJanusPublisher) Close(ctx context.Context) { notify := false p.mu.Lock() - if handle := p.handle.Load(); handle != nil && p.roomId != 0 { - destroy_msg := api.StringMap{ + if handle := p.handle; handle != nil && p.roomId != 0 { + destroy_msg := map[string]interface{}{ "request": "destroy", "room": p.roomId, } if _, err := handle.Request(ctx, destroy_msg); err != nil { - p.logger.Printf("Error destroying room %d: %s", p.roomId, err) + log.Printf("Error destroying room %d: %s", p.roomId, err) } else { - p.logger.Printf("Room %d destroyed", p.roomId) + log.Printf("Room %d destroyed", p.roomId) } p.mcu.mu.Lock() - delete(p.mcu.publishers, sfu.GetStreamId(p.id, p.streamType)) + delete(p.mcu.publishers, getStreamId(p.id, p.streamType)) p.mcu.mu.Unlock() p.roomId = 0 notify = true @@ -168,37 +156,26 @@ func (p *janusPublisher) Close(ctx context.Context) { p.stats.Reset() if notify { - sfuinternal.StatsPublishersCurrent.WithLabelValues(string(p.streamType)).Dec() + statsPublishersCurrent.WithLabelValues(string(p.streamType)).Dec() p.mcu.unregisterClient(p) p.listener.PublisherClosed(p) } - p.janusClient.Close(ctx) + p.mcuJanusClient.Close(ctx) } -func (p *janusPublisher) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { - sfuinternal.StatsMcuMessagesTotal.WithLabelValues(data.Type).Inc() +func (p *mcuJanusPublisher) SendMessage(ctx context.Context, message *MessageClientMessage, data *MessageClientMessageData, callback func(error, map[string]interface{})) { + statsMcuMessagesTotal.WithLabelValues(data.Type).Inc() jsep_msg := data.Payload switch data.Type { case "offer": p.deferred <- func() { - if data.OfferSdp == nil { + if data.offerSdp == nil { // Should have been checked before. - go callback(errors.New("no sdp found in offer"), nil) + go callback(errors.New("No sdp found in offer"), nil) return } - if api.FilterSDPCandidates(data.OfferSdp, p.mcu.settings.allowedCandidates.Load(), p.mcu.settings.blockedCandidates.Load()) { - // Update request with filtered SDP. - marshalled, err := data.OfferSdp.Marshal() - if err != nil { - go callback(fmt.Errorf("could not marshal filtered offer: %w", err), nil) - return - } - - jsep_msg["sdp"] = string(marshalled) - } - - p.offerSdp.Store(data.OfferSdp) + p.offerSdp.Store(data.offerSdp) p.sdpFlags.Add(sdpHasOffer) if p.sdpFlags.Get() == sdpHasAnswer|sdpHasOffer { p.sdpReady.Close() @@ -209,25 +186,32 @@ func (p *janusPublisher) SendMessage(ctx context.Context, message *api.MessageCl msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) defer cancel() - p.sendOffer(msgctx, jsep_msg, func(err error, jsep api.StringMap) { + p.sendOffer(msgctx, jsep_msg, func(err error, jsep map[string]interface{}) { if err != nil { callback(err, jsep) return } - sdpString, found := api.GetStringMapEntry[string](jsep, "sdp") + sdpData, found := jsep["sdp"] if !found { - p.logger.Printf("No/invalid sdp found in answer %+v", jsep) - } else if answerSdp, err := api.ParseSDP(sdpString); err != nil { - p.logger.Printf("Error parsing answer sdp %+v: %s", sdpString, err) - p.answerSdp.Store(nil) - p.sdpFlags.Remove(sdpHasAnswer) + log.Printf("No sdp found in answer %+v", jsep) } else { - // Note: we don't need to filter the SDP received from Janus. - p.answerSdp.Store(answerSdp) - p.sdpFlags.Add(sdpHasAnswer) - if p.sdpFlags.Get() == sdpHasAnswer|sdpHasOffer { - p.sdpReady.Close() + sdpString, ok := sdpData.(string) + if !ok { + log.Printf("Invalid sdp found in answer %+v", jsep) + } else { + var answerSdp sdp.SessionDescription + if err := answerSdp.UnmarshalString(sdpString); err != nil { + log.Printf("Error parsing answer sdp %+v: %s", sdpString, err) + p.answerSdp.Store(nil) + p.sdpFlags.Remove(sdpHasAnswer) + } else { + p.answerSdp.Store(&answerSdp) + p.sdpFlags.Add(sdpHasAnswer) + if p.sdpFlags.Get() == sdpHasAnswer|sdpHasOffer { + p.sdpReady.Close() + } + } } } @@ -235,11 +219,6 @@ func (p *janusPublisher) SendMessage(ctx context.Context, message *api.MessageCl }) } case "candidate": - if api.FilterCandidate(data.Candidate, p.mcu.settings.allowedCandidates.Load(), p.mcu.settings.blockedCandidates.Load()) { - go callback(api.ErrCandidateFiltered, nil) - return - } - p.deferred <- func() { msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) defer cancel() @@ -247,18 +226,19 @@ func (p *janusPublisher) SendMessage(ctx context.Context, message *api.MessageCl if data.Sid == "" || data.Sid == p.Sid() { p.sendCandidate(msgctx, jsep_msg["candidate"], callback) } else { - go callback(fmt.Errorf("candidate message sid (%s) does not match publisher sid (%s)", data.Sid, p.Sid()), nil) + go callback(fmt.Errorf("Candidate message sid (%s) does not match publisher sid (%s)", data.Sid, p.Sid()), nil) } } case "endOfCandidates": // Ignore default: - go callback(fmt.Errorf("unsupported message type: %s", data.Type), nil) + go callback(fmt.Errorf("Unsupported message type: %s", data.Type), nil) } } func getFmtpValue(fmtp string, key string) (string, bool) { - for part := range internal.SplitEntries(fmtp, ";") { + parts := strings.Split(fmtp, ";") + for _, part := range parts { kv := strings.SplitN(part, "=", 2) if len(kv) != 2 { continue @@ -272,7 +252,7 @@ func getFmtpValue(fmtp string, key string) (string, bool) { return "", false } -func (p *janusPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, error) { +func (p *mcuJanusPublisher) GetStreams(ctx context.Context) ([]PublisherStream, error) { offerSdp := p.offerSdp.Load() answerSdp := p.answerSdp.Load() if offerSdp == nil || answerSdp == nil { @@ -289,14 +269,14 @@ func (p *janusPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, } } - var streams []sfu.PublisherStream + var streams []PublisherStream for idx, m := range answerSdp.MediaDescriptions { mid, found := m.Attribute(sdp.AttrKeyMID) if !found { continue } - s := sfu.PublisherStream{ + s := PublisherStream{ Mid: mid, Mindex: idx, Type: m.MediaName.Media, @@ -322,8 +302,7 @@ func (p *janusPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, continue } - switch { - case strings.EqualFold(s.Type, "audio"): + if strings.EqualFold(s.Type, "audio") { s.Codec = answerCodec.Name if value, found := getFmtpValue(answerCodec.Fmtp, "useinbandfec"); found && value == "1" { s.Fec = true @@ -334,7 +313,7 @@ func (p *janusPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, if value, found := getFmtpValue(answerCodec.Fmtp, "stereo"); found && value == "1" { s.Stereo = true } - case strings.EqualFold(s.Type, "video"): + } else if strings.EqualFold(s.Type, "video") { s.Codec = answerCodec.Name // TODO: Determine if SVC is used. s.Svc = false @@ -358,7 +337,7 @@ func (p *janusPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, switch a.Key { case sdp.AttrKeyExtMap: if err := extmap.Unmarshal(extmap.Name() + ":" + a.Value); err != nil { - p.logger.Printf("Error parsing extmap %s: %s", a.Value, err) + log.Printf("Error parsing extmap %s: %s", a.Value, err) continue } @@ -388,10 +367,10 @@ func (p *janusPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, } } - case strings.EqualFold(s.Type, "data"): + } else if strings.EqualFold(s.Type, "data") { // nolint // Already handled above. - default: - p.logger.Printf("Skip type %s", s.Type) + } else { + log.Printf("Skip type %s", s.Type) continue } @@ -401,17 +380,12 @@ func (p *janusPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, return streams, nil } -func getPublisherRemoteId(id api.PublicSessionId, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) string { +func getPublisherRemoteId(id string, remoteId string, hostname string, port int, rtcpPort int) string { return fmt.Sprintf("%s-%s@%s:%d:%d", id, remoteId, hostname, port, rtcpPort) } -func (p *janusPublisher) PublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error { - handle := p.handle.Load() - if handle == nil { - return sfu.ErrNotConnected - } - - msg := api.StringMap{ +func (p *mcuJanusPublisher) PublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { + msg := map[string]interface{}{ "request": "publish_remotely", "room": p.roomId, "publisher_id": streamTypeUserIds[p.streamType], @@ -420,13 +394,13 @@ func (p *janusPublisher) PublishRemote(ctx context.Context, remoteId api.PublicS "port": port, "rtcp_port": rtcpPort, } - response, err := handle.Request(ctx, msg) + response, err := p.handle.Request(ctx, msg) if err != nil { return err } errorMessage := getPluginStringValue(response.PluginData, pluginVideoRoom, "error") - errorCode := getPluginIntValue(p.logger, response.PluginData, pluginVideoRoom, "error_code") + errorCode := getPluginIntValue(response.PluginData, pluginVideoRoom, "error_code") if errorMessage != "" || errorCode != 0 { if errorCode == 0 { errorCode = 500 @@ -443,29 +417,24 @@ func (p *janusPublisher) PublishRemote(ctx context.Context, remoteId api.PublicS } } - p.logger.Printf("Publishing %s to %s (port=%d, rtcpPort=%d) for %s", p.id, hostname, port, rtcpPort, remoteId) + log.Printf("Publishing %s to %s (port=%d, rtcpPort=%d) for %s", p.id, hostname, port, rtcpPort, remoteId) return nil } -func (p *janusPublisher) UnpublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error { - handle := p.handle.Load() - if handle == nil { - return sfu.ErrNotConnected - } - - msg := api.StringMap{ +func (p *mcuJanusPublisher) UnpublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { + msg := map[string]interface{}{ "request": "unpublish_remotely", "room": p.roomId, "publisher_id": streamTypeUserIds[p.streamType], "remote_id": getPublisherRemoteId(p.id, remoteId, hostname, port, rtcpPort), } - response, err := handle.Request(ctx, msg) + response, err := p.handle.Request(ctx, msg) if err != nil { return err } errorMessage := getPluginStringValue(response.PluginData, pluginVideoRoom, "error") - errorCode := getPluginIntValue(p.logger, response.PluginData, pluginVideoRoom, "error_code") + errorCode := getPluginIntValue(response.PluginData, pluginVideoRoom, "error_code") if errorMessage != "" || errorCode != 0 { if errorCode == 0 { errorCode = 500 @@ -482,6 +451,6 @@ func (p *janusPublisher) UnpublishRemote(ctx context.Context, remoteId api.Publi } } - p.logger.Printf("Unpublished remote %s for %s", p.id, remoteId) + log.Printf("Unpublished remote %s for %s", p.id, remoteId) return nil } diff --git a/mcu_janus_publisher_test.go b/mcu_janus_publisher_test.go new file mode 100644 index 0000000..ab7d96a --- /dev/null +++ b/mcu_janus_publisher_test.go @@ -0,0 +1,96 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetFmtpValueH264(t *testing.T) { + assert := assert.New(t) + testcases := []struct { + fmtp string + profile string + }{ + { + "", + "", + }, + { + "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", + "42001f", + }, + { + "level-asymmetry-allowed=1;packetization-mode=0", + "", + }, + { + "level-asymmetry-allowed=1; packetization-mode=0; profile-level-id = 42001f", + "42001f", + }, + } + + for _, tc := range testcases { + value, found := getFmtpValue(tc.fmtp, "profile-level-id") + if !found && tc.profile != "" { + assert.Fail("did not find profile \"%s\" in \"%s\"", tc.profile, tc.fmtp) + } else if found && tc.profile == "" { + assert.Fail("did not expect profile in \"%s\" but got \"%s\"", tc.fmtp, value) + } else if found && tc.profile != value { + assert.Fail("expected profile \"%s\" in \"%s\" but got \"%s\"", tc.profile, tc.fmtp, value) + } + } +} + +func TestGetFmtpValueVP9(t *testing.T) { + assert := assert.New(t) + testcases := []struct { + fmtp string + profile string + }{ + { + "", + "", + }, + { + "profile-id=0", + "0", + }, + { + "profile-id = 0", + "0", + }, + } + + for _, tc := range testcases { + value, found := getFmtpValue(tc.fmtp, "profile-id") + if !found && tc.profile != "" { + assert.Fail("did not find profile \"%s\" in \"%s\"", tc.profile, tc.fmtp) + } else if found && tc.profile == "" { + assert.Fail("did not expect profile in \"%s\" but got \"%s\"", tc.fmtp, value) + } else if found && tc.profile != value { + assert.Fail("expected profile \"%s\" in \"%s\" but got \"%s\"", tc.profile, tc.fmtp, value) + } + } +} diff --git a/mcu_janus_remote_publisher.go b/mcu_janus_remote_publisher.go new file mode 100644 index 0000000..9a3575b --- /dev/null +++ b/mcu_janus_remote_publisher.go @@ -0,0 +1,156 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "log" + "sync/atomic" + + "github.com/notedit/janus-go" +) + +type mcuJanusRemotePublisher struct { + mcuJanusPublisher + + ref atomic.Int64 + + controller RemotePublisherController + + port int + rtcpPort int +} + +func (p *mcuJanusRemotePublisher) addRef() int64 { + return p.ref.Add(1) +} + +func (p *mcuJanusRemotePublisher) release() bool { + return p.ref.Add(-1) == 0 +} + +func (p *mcuJanusRemotePublisher) Port() int { + return p.port +} + +func (p *mcuJanusRemotePublisher) RtcpPort() int { + return p.rtcpPort +} + +func (p *mcuJanusRemotePublisher) handleEvent(event *janus.EventMsg) { + if videoroom := getPluginStringValue(event.Plugindata, pluginVideoRoom, "videoroom"); videoroom != "" { + ctx := context.TODO() + switch videoroom { + case "destroyed": + log.Printf("Remote publisher %d: associated room has been destroyed, closing", p.handleId) + go p.Close(ctx) + case "slow_link": + // Ignore, processed through "handleSlowLink" in the general events. + default: + log.Printf("Unsupported videoroom remote publisher event in %d: %+v", p.handleId, event) + } + } else { + log.Printf("Unsupported remote publisher event in %d: %+v", p.handleId, event) + } +} + +func (p *mcuJanusRemotePublisher) handleHangup(event *janus.HangupMsg) { + log.Printf("Remote publisher %d received hangup (%s), closing", p.handleId, event.Reason) + go p.Close(context.Background()) +} + +func (p *mcuJanusRemotePublisher) handleDetached(event *janus.DetachedMsg) { + log.Printf("Remote publisher %d received detached, closing", p.handleId) + go p.Close(context.Background()) +} + +func (p *mcuJanusRemotePublisher) handleConnected(event *janus.WebRTCUpMsg) { + log.Printf("Remote publisher %d received connected", p.handleId) + p.mcu.publisherConnected.Notify(getStreamId(p.id, p.streamType)) +} + +func (p *mcuJanusRemotePublisher) handleSlowLink(event *janus.SlowLinkMsg) { + if event.Uplink { + log.Printf("Remote publisher %s (%d) is reporting %d lost packets on the uplink (Janus -> client)", p.listener.PublicId(), p.handleId, event.Lost) + } else { + log.Printf("Remote publisher %s (%d) is reporting %d lost packets on the downlink (client -> Janus)", p.listener.PublicId(), p.handleId, event.Lost) + } +} + +func (p *mcuJanusRemotePublisher) NotifyReconnected() { + ctx := context.TODO() + handle, session, roomId, _, err := p.mcu.getOrCreatePublisherHandle(ctx, p.id, p.streamType, p.settings) + if err != nil { + log.Printf("Could not reconnect remote publisher %s: %s", p.id, err) + // TODO(jojo): Retry + return + } + + p.handle = handle + p.handleId = handle.Id + p.session = session + p.roomId = roomId + + log.Printf("Remote publisher %s reconnected on handle %d", p.id, p.handleId) +} + +func (p *mcuJanusRemotePublisher) Close(ctx context.Context) { + if !p.release() { + return + } + + if err := p.controller.StopPublishing(ctx, p); err != nil { + log.Printf("Error stopping remote publisher %s in room %d: %s", p.id, p.roomId, err) + } + + p.mu.Lock() + if handle := p.handle; handle != nil { + response, err := p.handle.Request(ctx, map[string]interface{}{ + "request": "remove_remote_publisher", + "room": p.roomId, + "id": streamTypeUserIds[p.streamType], + }) + if err != nil { + log.Printf("Error removing remote publisher %s in room %d: %s", p.id, p.roomId, err) + } else { + log.Printf("Removed remote publisher: %+v", response) + } + if p.roomId != 0 { + destroy_msg := map[string]interface{}{ + "request": "destroy", + "room": p.roomId, + } + if _, err := handle.Request(ctx, destroy_msg); err != nil { + log.Printf("Error destroying room %d: %s", p.roomId, err) + } else { + log.Printf("Room %d destroyed", p.roomId) + } + p.mcu.mu.Lock() + delete(p.mcu.remotePublishers, getStreamId(p.id, p.streamType)) + p.mcu.mu.Unlock() + p.roomId = 0 + } + } + + p.closeClient(ctx) + p.mu.Unlock() +} diff --git a/sfu/janus/remote_subscriber.go b/mcu_janus_remote_subscriber.go similarity index 51% rename from sfu/janus/remote_subscriber.go rename to mcu_janus_remote_subscriber.go index a8c12e1..8f7fe5e 100644 --- a/sfu/janus/remote_subscriber.go +++ b/mcu_janus_remote_subscriber.go @@ -19,28 +19,29 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package janus +package signaling import ( "context" + "log" "strconv" "sync/atomic" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" + "github.com/notedit/janus-go" ) -type janusRemoteSubscriber struct { - janusSubscriber +type mcuJanusRemoteSubscriber struct { + mcuJanusSubscriber - remote atomic.Pointer[janusRemotePublisher] + remote atomic.Pointer[mcuJanusRemotePublisher] } -func (p *janusRemoteSubscriber) handleEvent(event *janus.EventMsg) { +func (p *mcuJanusRemoteSubscriber) handleEvent(event *janus.EventMsg) { if videoroom := getPluginStringValue(event.Plugindata, pluginVideoRoom, "videoroom"); videoroom != "" { ctx := context.TODO() switch videoroom { case "destroyed": - p.logger.Printf("Remote subscriber %d: associated room has been destroyed, closing", p.handleId.Load()) + log.Printf("Remote subscriber %d: associated room has been destroyed, closing", p.handleId) go p.Close(ctx) case "event": // Handle renegotiations, but ignore other events like selected @@ -52,65 +53,61 @@ func (p *janusRemoteSubscriber) handleEvent(event *janus.EventMsg) { case "slow_link": // Ignore, processed through "handleSlowLink" in the general events. default: - p.logger.Printf("Unsupported videoroom event %s for remote subscriber %d: %+v", videoroom, p.handleId.Load(), event) + log.Printf("Unsupported videoroom event %s for remote subscriber %d: %+v", videoroom, p.handleId, event) } } else { - p.logger.Printf("Unsupported event for remote subscriber %d: %+v", p.handleId.Load(), event) + log.Printf("Unsupported event for remote subscriber %d: %+v", p.handleId, event) } } -func (p *janusRemoteSubscriber) handleHangup(event *janus.HangupMsg) { - p.logger.Printf("Remote subscriber %d received hangup (%s), closing", p.handleId.Load(), event.Reason) +func (p *mcuJanusRemoteSubscriber) handleHangup(event *janus.HangupMsg) { + log.Printf("Remote subscriber %d received hangup (%s), closing", p.handleId, event.Reason) go p.Close(context.Background()) } -func (p *janusRemoteSubscriber) handleDetached(event *janus.DetachedMsg) { - p.logger.Printf("Remote subscriber %d received detached, closing", p.handleId.Load()) +func (p *mcuJanusRemoteSubscriber) handleDetached(event *janus.DetachedMsg) { + log.Printf("Remote subscriber %d received detached, closing", p.handleId) go p.Close(context.Background()) } -func (p *janusRemoteSubscriber) handleConnected(event *janus.WebRTCUpMsg) { - p.logger.Printf("Remote subscriber %d received connected", p.handleId.Load()) +func (p *mcuJanusRemoteSubscriber) handleConnected(event *janus.WebRTCUpMsg) { + log.Printf("Remote subscriber %d received connected", p.handleId) p.mcu.SubscriberConnected(p.Id(), p.publisher, p.streamType) } -func (p *janusRemoteSubscriber) handleSlowLink(event *janus.SlowLinkMsg) { +func (p *mcuJanusRemoteSubscriber) handleSlowLink(event *janus.SlowLinkMsg) { if event.Uplink { - p.logger.Printf("Remote subscriber %s (%d) is reporting %d lost packets on the uplink (Janus -> client)", p.listener.PublicId(), p.handleId.Load(), event.Lost) + log.Printf("Remote subscriber %s (%d) is reporting %d lost packets on the uplink (Janus -> client)", p.listener.PublicId(), p.handleId, event.Lost) } else { - p.logger.Printf("Remote subscriber %s (%d) is reporting %d lost packets on the downlink (client -> Janus)", p.listener.PublicId(), p.handleId.Load(), event.Lost) + log.Printf("Remote subscriber %s (%d) is reporting %d lost packets on the downlink (client -> Janus)", p.listener.PublicId(), p.handleId, event.Lost) } } -func (p *janusRemoteSubscriber) handleMedia(event *janus.MediaMsg) { +func (p *mcuJanusRemoteSubscriber) handleMedia(event *janus.MediaMsg) { // Only triggered for publishers } -func (p *janusRemoteSubscriber) NotifyReconnected() { +func (p *mcuJanusRemoteSubscriber) NotifyReconnected() { ctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) defer cancel() handle, pub, err := p.mcu.getOrCreateSubscriberHandle(ctx, p.publisher, p.streamType) if err != nil { // TODO(jojo): Retry? - p.logger.Printf("Could not reconnect remote subscriber for publisher %s: %s", p.publisher, err) + log.Printf("Could not reconnect remote subscriber for publisher %s: %s", p.publisher, err) p.Close(context.Background()) return } - if prev := p.handle.Swap(handle); prev != nil { - if _, err := prev.Detach(context.Background()); err != nil { - p.logger.Printf("Error detaching old remote subscriber handle %d: %s", prev.Id, err) - } - } - p.handleId.Store(handle.Id) + p.handle = handle + p.handleId = handle.Id p.roomId = pub.roomId p.sid = strconv.FormatUint(handle.Id, 10) p.listener.SubscriberSidUpdated(p) - p.logger.Printf("Subscriber %d for publisher %s reconnected on handle %d", p.id, p.publisher, p.handleId.Load()) + log.Printf("Subscriber %d for publisher %s reconnected on handle %d", p.id, p.publisher, p.handleId) } -func (p *janusRemoteSubscriber) Close(ctx context.Context) { - p.janusSubscriber.Close(ctx) +func (p *mcuJanusRemoteSubscriber) Close(ctx context.Context) { + p.mcuJanusSubscriber.Close(ctx) if remote := p.remote.Swap(nil); remote != nil { remote.Close(context.Background()) diff --git a/sfu/janus/stream_selection.go b/mcu_janus_stream_selection.go similarity index 52% rename from sfu/janus/stream_selection.go rename to mcu_janus_stream_selection.go index f6430f7..9381ef3 100644 --- a/sfu/janus/stream_selection.go +++ b/mcu_janus_stream_selection.go @@ -19,84 +19,90 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package janus +package signaling import ( + "database/sql" "fmt" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" ) type streamSelection struct { - substream *int - temporal *int - audio *bool - video *bool + substream sql.NullInt16 + temporal sql.NullInt16 + audio sql.NullBool + video sql.NullBool } func (s *streamSelection) HasValues() bool { - return s.substream != nil || s.temporal != nil || s.audio != nil || s.video != nil + return s.substream.Valid || s.temporal.Valid || s.audio.Valid || s.video.Valid } -func (s *streamSelection) AddToMessage(message api.StringMap) { - if s.substream != nil { - message["substream"] = *s.substream +func (s *streamSelection) AddToMessage(message map[string]interface{}) { + if s.substream.Valid { + message["substream"] = s.substream.Int16 } - if s.temporal != nil { - message["temporal"] = *s.temporal + if s.temporal.Valid { + message["temporal"] = s.temporal.Int16 } - if s.audio != nil { - message["audio"] = *s.audio + if s.audio.Valid { + message["audio"] = s.audio.Bool } - if s.video != nil { - message["video"] = *s.video + if s.video.Valid { + message["video"] = s.video.Bool } } -func parseStreamSelection(payload api.StringMap) (*streamSelection, error) { +func parseStreamSelection(payload map[string]interface{}) (*streamSelection, error) { var stream streamSelection if value, found := payload["substream"]; found { switch value := value.(type) { case int: - stream.substream = &value + stream.substream.Valid = true + stream.substream.Int16 = int16(value) case float32: - stream.substream = internal.MakePtr(int(value)) + stream.substream.Valid = true + stream.substream.Int16 = int16(value) case float64: - stream.substream = internal.MakePtr(int(value)) + stream.substream.Valid = true + stream.substream.Int16 = int16(value) default: - return nil, fmt.Errorf("unsupported substream value: %v", value) + return nil, fmt.Errorf("Unsupported substream value: %v", value) } } if value, found := payload["temporal"]; found { switch value := value.(type) { case int: - stream.temporal = &value + stream.temporal.Valid = true + stream.temporal.Int16 = int16(value) case float32: - stream.temporal = internal.MakePtr(int(value)) + stream.temporal.Valid = true + stream.temporal.Int16 = int16(value) case float64: - stream.temporal = internal.MakePtr(int(value)) + stream.temporal.Valid = true + stream.temporal.Int16 = int16(value) default: - return nil, fmt.Errorf("unsupported temporal value: %v", value) + return nil, fmt.Errorf("Unsupported temporal value: %v", value) } } if value, found := payload["audio"]; found { switch value := value.(type) { case bool: - stream.audio = &value + stream.audio.Valid = true + stream.audio.Bool = value default: - return nil, fmt.Errorf("unsupported audio value: %v", value) + return nil, fmt.Errorf("Unsupported audio value: %v", value) } } if value, found := payload["video"]; found { switch value := value.(type) { case bool: - stream.video = &value + stream.video.Valid = true + stream.video.Bool = value default: - return nil, fmt.Errorf("unsupported video value: %v", value) + return nil, fmt.Errorf("Unsupported video value: %v", value) } } diff --git a/mcu_janus_subscriber.go b/mcu_janus_subscriber.go new file mode 100644 index 0000000..b79575f --- /dev/null +++ b/mcu_janus_subscriber.go @@ -0,0 +1,321 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/notedit/janus-go" +) + +type mcuJanusSubscriber struct { + mcuJanusClient + + publisher string +} + +func (p *mcuJanusSubscriber) Publisher() string { + return p.publisher +} + +func (p *mcuJanusSubscriber) handleEvent(event *janus.EventMsg) { + if videoroom := getPluginStringValue(event.Plugindata, pluginVideoRoom, "videoroom"); videoroom != "" { + ctx := context.TODO() + switch videoroom { + case "destroyed": + log.Printf("Subscriber %d: associated room has been destroyed, closing", p.handleId) + go p.Close(ctx) + case "event": + // Handle renegotiations, but ignore other events like selected + // substream / temporal layer. + if getPluginStringValue(event.Plugindata, pluginVideoRoom, "configured") == "ok" && + event.Jsep != nil && event.Jsep["type"] == "offer" && event.Jsep["sdp"] != nil { + p.listener.OnUpdateOffer(p, event.Jsep) + } + case "slow_link": + // Ignore, processed through "handleSlowLink" in the general events. + default: + log.Printf("Unsupported videoroom event %s for subscriber %d: %+v", videoroom, p.handleId, event) + } + } else { + log.Printf("Unsupported event for subscriber %d: %+v", p.handleId, event) + } +} + +func (p *mcuJanusSubscriber) handleHangup(event *janus.HangupMsg) { + log.Printf("Subscriber %d received hangup (%s), closing", p.handleId, event.Reason) + go p.Close(context.Background()) +} + +func (p *mcuJanusSubscriber) handleDetached(event *janus.DetachedMsg) { + log.Printf("Subscriber %d received detached, closing", p.handleId) + go p.Close(context.Background()) +} + +func (p *mcuJanusSubscriber) handleConnected(event *janus.WebRTCUpMsg) { + log.Printf("Subscriber %d received connected", p.handleId) + p.mcu.SubscriberConnected(p.Id(), p.publisher, p.streamType) +} + +func (p *mcuJanusSubscriber) handleSlowLink(event *janus.SlowLinkMsg) { + if event.Uplink { + log.Printf("Subscriber %s (%d) is reporting %d lost packets on the uplink (Janus -> client)", p.listener.PublicId(), p.handleId, event.Lost) + } else { + log.Printf("Subscriber %s (%d) is reporting %d lost packets on the downlink (client -> Janus)", p.listener.PublicId(), p.handleId, event.Lost) + } +} + +func (p *mcuJanusSubscriber) handleMedia(event *janus.MediaMsg) { + // Only triggered for publishers +} + +func (p *mcuJanusSubscriber) NotifyReconnected() { + ctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) + defer cancel() + handle, pub, err := p.mcu.getOrCreateSubscriberHandle(ctx, p.publisher, p.streamType) + if err != nil { + // TODO(jojo): Retry? + log.Printf("Could not reconnect subscriber for publisher %s: %s", p.publisher, err) + p.Close(context.Background()) + return + } + + p.handle = handle + p.handleId = handle.Id + p.roomId = pub.roomId + p.sid = strconv.FormatUint(handle.Id, 10) + p.listener.SubscriberSidUpdated(p) + log.Printf("Subscriber %d for publisher %s reconnected on handle %d", p.id, p.publisher, p.handleId) +} + +func (p *mcuJanusSubscriber) Close(ctx context.Context) { + p.mu.Lock() + closed := p.closeClient(ctx) + p.mu.Unlock() + + if closed { + p.mcu.SubscriberDisconnected(p.Id(), p.publisher, p.streamType) + statsSubscribersCurrent.WithLabelValues(string(p.streamType)).Dec() + } + p.mcu.unregisterClient(p) + p.listener.SubscriberClosed(p) + p.mcuJanusClient.Close(ctx) +} + +func (p *mcuJanusSubscriber) joinRoom(ctx context.Context, stream *streamSelection, callback func(error, map[string]interface{})) { + handle := p.handle + if handle == nil { + callback(ErrNotConnected, nil) + return + } + + waiter := p.mcu.publisherConnected.NewWaiter(getStreamId(p.publisher, p.streamType)) + defer p.mcu.publisherConnected.Release(waiter) + + loggedNotPublishingYet := false +retry: + join_msg := map[string]interface{}{ + "request": "join", + "ptype": "subscriber", + "room": p.roomId, + } + if p.mcu.isMultistream() { + join_msg["streams"] = []map[string]interface{}{ + { + "feed": streamTypeUserIds[p.streamType], + }, + } + } else { + join_msg["feed"] = streamTypeUserIds[p.streamType] + } + if stream != nil { + stream.AddToMessage(join_msg) + } + join_response, err := handle.Message(ctx, join_msg, nil) + if err != nil { + callback(err, nil) + return + } + + if error_code := getPluginIntValue(join_response.Plugindata, pluginVideoRoom, "error_code"); error_code > 0 { + switch error_code { + case JANUS_VIDEOROOM_ERROR_ALREADY_JOINED: + // The subscriber is already connected to the room. This can happen + // if a client leaves a call but keeps the subscriber objects active. + // On joining the call again, the subscriber tries to join on the + // MCU which will fail because he is still connected. + // To get a new Offer SDP, we have to tear down the session on the + // MCU and join again. + p.mu.Lock() + p.closeClient(ctx) + p.mu.Unlock() + + var pub *mcuJanusPublisher + handle, pub, err = p.mcu.getOrCreateSubscriberHandle(ctx, p.publisher, p.streamType) + if err != nil { + // Reconnection didn't work, need to unregister/remove subscriber + // so a new object will be created if the request is retried. + p.mcu.unregisterClient(p) + p.listener.SubscriberClosed(p) + callback(fmt.Errorf("Already connected as subscriber for %s, error during re-joining: %s", p.streamType, err), nil) + return + } + + p.handle = handle + p.handleId = handle.Id + p.roomId = pub.roomId + p.sid = strconv.FormatUint(handle.Id, 10) + p.listener.SubscriberSidUpdated(p) + p.closeChan = make(chan struct{}, 1) + go p.run(p.handle, p.closeChan) + log.Printf("Already connected subscriber %d for %s, leaving and re-joining on handle %d", p.id, p.streamType, p.handleId) + goto retry + case JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM: + fallthrough + case JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED: + switch error_code { + case JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM: + log.Printf("Publisher %s not created yet for %s, wait and retry to join room %d as subscriber", p.publisher, p.streamType, p.roomId) + case JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED: + log.Printf("Publisher %s not sending yet for %s, wait and retry to join room %d as subscriber", p.publisher, p.streamType, p.roomId) + } + + if !loggedNotPublishingYet { + loggedNotPublishingYet = true + statsWaitingForPublisherTotal.WithLabelValues(string(p.streamType)).Inc() + } + + if err := waiter.Wait(ctx); err != nil { + callback(err, nil) + return + } + log.Printf("Retry subscribing %s from %s", p.streamType, p.publisher) + goto retry + default: + // TODO(jojo): Should we handle other errors, too? + callback(fmt.Errorf("Error joining room as subscriber: %+v", join_response), nil) + return + } + } + //log.Println("Joined as listener", join_response) + + p.session = join_response.Session + callback(nil, join_response.Jsep) +} + +func (p *mcuJanusSubscriber) update(ctx context.Context, stream *streamSelection, callback func(error, map[string]interface{})) { + handle := p.handle + if handle == nil { + callback(ErrNotConnected, nil) + return + } + + configure_msg := map[string]interface{}{ + "request": "configure", + "update": true, + } + if stream != nil { + stream.AddToMessage(configure_msg) + } + configure_response, err := handle.Message(ctx, configure_msg, nil) + if err != nil { + callback(err, nil) + return + } + + callback(nil, configure_response.Jsep) +} + +func (p *mcuJanusSubscriber) SendMessage(ctx context.Context, message *MessageClientMessage, data *MessageClientMessageData, callback func(error, map[string]interface{})) { + statsMcuMessagesTotal.WithLabelValues(data.Type).Inc() + jsep_msg := data.Payload + switch data.Type { + case "requestoffer": + fallthrough + case "sendoffer": + p.deferred <- func() { + msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) + defer cancel() + + stream, err := parseStreamSelection(jsep_msg) + if err != nil { + go callback(err, nil) + return + } + + if data.Sid == "" || data.Sid != p.Sid() { + p.joinRoom(msgctx, stream, callback) + } else { + p.update(msgctx, stream, callback) + } + } + case "answer": + p.deferred <- func() { + msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) + defer cancel() + + if data.Sid == "" || data.Sid == p.Sid() { + p.sendAnswer(msgctx, jsep_msg, callback) + } else { + go callback(fmt.Errorf("Answer message sid (%s) does not match subscriber sid (%s)", data.Sid, p.Sid()), nil) + } + } + case "candidate": + p.deferred <- func() { + msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) + defer cancel() + + if data.Sid == "" || data.Sid == p.Sid() { + p.sendCandidate(msgctx, jsep_msg["candidate"], callback) + } else { + go callback(fmt.Errorf("Candidate message sid (%s) does not match subscriber sid (%s)", data.Sid, p.Sid()), nil) + } + } + case "endOfCandidates": + // Ignore + case "selectStream": + stream, err := parseStreamSelection(jsep_msg) + if err != nil { + go callback(err, nil) + return + } + + if stream == nil || !stream.HasValues() { + // Nothing to do + go callback(nil, nil) + return + } + + p.deferred <- func() { + msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) + defer cancel() + + p.selectStream(msgctx, stream, callback) + } + default: + // Return error asynchronously + go callback(fmt.Errorf("Unsupported message type: %s", data.Type), nil) + } +} diff --git a/mcu_janus_test.go b/mcu_janus_test.go new file mode 100644 index 0000000..f7407d8 --- /dev/null +++ b/mcu_janus_test.go @@ -0,0 +1,584 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "encoding/json" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/dlintw/goconf" + "github.com/notedit/janus-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type TestJanusHandle struct { + id uint64 +} + +type TestJanusRoom struct { + id uint64 +} + +type TestJanusHandler func(room *TestJanusRoom, body map[string]interface{}) (interface{}, *janus.ErrorMsg) + +type TestJanusGateway struct { + t *testing.T + + sid atomic.Uint64 + tid atomic.Uint64 + hid atomic.Uint64 + rid atomic.Uint64 + mu sync.Mutex + + sessions map[uint64]*JanusSession + transactions map[uint64]*transaction + handles map[uint64]*TestJanusHandle + rooms map[uint64]*TestJanusRoom + handlers map[string]TestJanusHandler +} + +func NewTestJanusGateway(t *testing.T) *TestJanusGateway { + gateway := &TestJanusGateway{ + t: t, + + sessions: make(map[uint64]*JanusSession), + transactions: make(map[uint64]*transaction), + handles: make(map[uint64]*TestJanusHandle), + rooms: make(map[uint64]*TestJanusRoom), + handlers: make(map[string]TestJanusHandler), + } + + t.Cleanup(func() { + assert := assert.New(t) + gateway.mu.Lock() + defer gateway.mu.Unlock() + assert.Len(gateway.sessions, 0) + assert.Len(gateway.transactions, 0) + assert.Len(gateway.handles, 0) + assert.Len(gateway.rooms, 0) + }) + + return gateway +} + +func (g *TestJanusGateway) registerHandlers(handlers map[string]TestJanusHandler) { + g.mu.Lock() + defer g.mu.Unlock() + for name, handler := range handlers { + g.handlers[name] = handler + } +} + +func (g *TestJanusGateway) Info(ctx context.Context) (*InfoMsg, error) { + return &InfoMsg{ + Name: "TestJanus", + Version: 1400, + VersionString: "1.4.0", + Author: "struktur AG", + DataChannels: true, + FullTrickle: true, + Plugins: map[string]janus.PluginInfo{ + pluginVideoRoom: { + Name: "Test VideoRoom plugin", + VersionString: "0.0.0", + Author: "struktur AG", + }, + }, + }, nil +} + +func (g *TestJanusGateway) Create(ctx context.Context) (*JanusSession, error) { + sid := g.sid.Add(1) + session := &JanusSession{ + Id: sid, + Handles: make(map[uint64]*JanusHandle), + gateway: g, + } + g.mu.Lock() + defer g.mu.Unlock() + g.sessions[sid] = session + return session, nil +} + +func (g *TestJanusGateway) Close() error { + return nil +} + +func (g *TestJanusGateway) processMessage(session *JanusSession, handle *TestJanusHandle, body map[string]interface{}) interface{} { + request := body["request"].(string) + switch request { + case "create": + room := &TestJanusRoom{ + id: g.rid.Add(1), + } + g.rooms[room.id] = room + + return &janus.SuccessMsg{ + PluginData: janus.PluginData{ + Plugin: pluginVideoRoom, + Data: map[string]interface{}{ + "room": room.id, + }, + }, + } + case "join": + rid := body["room"].(float64) + room := g.rooms[uint64(rid)] + if room == nil { + return &janus.ErrorMsg{ + Err: janus.ErrorData{ + Code: JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, + Reason: "Room not found", + }, + } + } + + assert.Equal(g.t, "publisher", body["ptype"]) + return &janus.EventMsg{ + Session: session.Id, + Handle: handle.id, + Plugindata: janus.PluginData{ + Plugin: pluginVideoRoom, + Data: map[string]interface{}{ + "room": room.id, + }, + }, + } + case "destroy": + rid := body["room"].(float64) + room := g.rooms[uint64(rid)] + if room == nil { + return &janus.ErrorMsg{ + Err: janus.ErrorData{ + Code: JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, + Reason: "Room not found", + }, + } + } + + delete(g.rooms, uint64(rid)) + + return &janus.SuccessMsg{ + PluginData: janus.PluginData{ + Plugin: pluginVideoRoom, + Data: map[string]interface{}{}, + }, + } + default: + rid := body["room"].(float64) + room := g.rooms[uint64(rid)] + if room == nil { + return &janus.ErrorMsg{ + Err: janus.ErrorData{ + Code: JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, + Reason: "Room not found", + }, + } + } + + handler, found := g.handlers[request] + if found { + var err *janus.ErrorMsg + result, err := handler(room, body) + if err != nil { + result = err + } + return result + } + } + + return nil +} + +func (g *TestJanusGateway) processRequest(msg map[string]interface{}) interface{} { + method, found := msg["janus"] + if !found { + return nil + } + + sid := msg["session_id"].(float64) + g.mu.Lock() + defer g.mu.Unlock() + session := g.sessions[uint64(sid)] + if session == nil { + return &janus.ErrorMsg{ + Err: janus.ErrorData{ + Code: JANUS_ERROR_SESSION_NOT_FOUND, + Reason: "Session not found", + }, + } + } + + switch method { + case "attach": + handle := &TestJanusHandle{ + id: g.hid.Add(1), + } + + g.handles[handle.id] = handle + + return &janus.SuccessMsg{ + Data: janus.SuccessData{ + ID: handle.id, + }, + } + case "detach": + hid := msg["handle_id"].(float64) + handle, found := g.handles[uint64(hid)] + if found { + delete(g.handles, handle.id) + } + if handle == nil { + return &janus.ErrorMsg{ + Err: janus.ErrorData{ + Code: JANUS_ERROR_HANDLE_NOT_FOUND, + Reason: "Handle not found", + }, + } + } + + return &janus.AckMsg{} + case "destroy": + delete(g.sessions, session.Id) + return &janus.AckMsg{} + case "message": + hid := msg["handle_id"].(float64) + handle, found := g.handles[uint64(hid)] + if !found { + return &janus.ErrorMsg{ + Err: janus.ErrorData{ + Code: JANUS_ERROR_HANDLE_NOT_FOUND, + Reason: "Handle not found", + }, + } + } + + body := msg["body"].(map[string]interface{}) + return g.processMessage(session, handle, body) + } + + return nil +} + +func (g *TestJanusGateway) send(msg map[string]interface{}, t *transaction) (uint64, error) { + tid := g.tid.Add(1) + + data, err := json.Marshal(msg) + require.NoError(g.t, err) + err = json.Unmarshal(data, &msg) + require.NoError(g.t, err) + + go t.run() + + g.mu.Lock() + defer g.mu.Unlock() + g.transactions[tid] = t + + go func() { + result := g.processRequest(msg) + if !assert.NotNil(g.t, result, "Unsupported request %+v", msg) { + result = &janus.ErrorMsg{ + Err: janus.ErrorData{ + Code: JANUS_ERROR_UNKNOWN, + Reason: "Not implemented", + }, + } + } + + t.add(result) + }() + + return tid, nil +} + +func (g *TestJanusGateway) removeTransaction(id uint64) { + g.mu.Lock() + defer g.mu.Unlock() + delete(g.transactions, id) +} + +func (g *TestJanusGateway) removeSession(session *JanusSession) { + g.mu.Lock() + defer g.mu.Unlock() + delete(g.sessions, session.Id) +} + +func newMcuJanusForTesting(t *testing.T) (*mcuJanus, *TestJanusGateway) { + gateway := NewTestJanusGateway(t) + + config := goconf.NewConfigFile() + mcu, err := NewMcuJanus(context.Background(), "", config) + require.NoError(t, err) + t.Cleanup(func() { + mcu.Stop() + }) + + mcuJanus := mcu.(*mcuJanus) + mcuJanus.createJanusGateway = func(ctx context.Context, wsURL string, listener GatewayListener) (JanusGatewayInterface, error) { + return gateway, nil + } + require.NoError(t, mcu.Start(context.Background())) + return mcuJanus, gateway +} + +type TestMcuListener struct { + id string +} + +func (t *TestMcuListener) PublicId() string { + return t.id +} + +func (t *TestMcuListener) OnUpdateOffer(client McuClient, offer map[string]interface{}) { + +} + +func (t *TestMcuListener) OnIceCandidate(client McuClient, candidate interface{}) { + +} + +func (t *TestMcuListener) OnIceCompleted(client McuClient) { + +} + +func (t *TestMcuListener) SubscriberSidUpdated(subscriber McuSubscriber) { + +} + +func (t *TestMcuListener) PublisherClosed(publisher McuPublisher) { + +} + +func (t *TestMcuListener) SubscriberClosed(subscriber McuSubscriber) { + +} + +type TestMcuController struct { + id string +} + +func (c *TestMcuController) PublisherId() string { + return c.id +} + +func (c *TestMcuController) StartPublishing(ctx context.Context, publisher McuRemotePublisherProperties) error { + // TODO: Check parameters? + return nil +} + +func (c *TestMcuController) StopPublishing(ctx context.Context, publisher McuRemotePublisherProperties) error { + // TODO: Check parameters? + return nil +} + +func (c *TestMcuController) GetStreams(ctx context.Context) ([]PublisherStream, error) { + streams := []PublisherStream{ + { + Mid: "0", + Mindex: 0, + Type: "audio", + Codec: "opus", + }, + } + return streams, nil +} + +type TestMcuInitiator struct { + country string +} + +func (i *TestMcuInitiator) Country() string { + return i.country +} + +func Test_JanusPublisherSubscriber(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + require := require.New(t) + + mcu, gateway := newMcuJanusForTesting(t) + gateway.registerHandlers(map[string]TestJanusHandler{}) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "publisher-id" + listener1 := &TestMcuListener{ + id: pubId, + } + + settings1 := NewPublisherSettings{} + initiator1 := &TestMcuInitiator{ + country: "DE", + } + + pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", StreamTypeVideo, settings1, initiator1) + require.NoError(err) + defer pub.Close(context.Background()) + + listener2 := &TestMcuListener{ + id: pubId, + } + + initiator2 := &TestMcuInitiator{ + country: "DE", + } + sub, err := mcu.NewSubscriber(ctx, listener2, pubId, StreamTypeVideo, initiator2) + require.NoError(err) + defer sub.Close(context.Background()) +} + +func Test_JanusSubscriberPublisher(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + require := require.New(t) + + mcu, gateway := newMcuJanusForTesting(t) + gateway.registerHandlers(map[string]TestJanusHandler{}) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "publisher-id" + listener1 := &TestMcuListener{ + id: pubId, + } + + settings1 := NewPublisherSettings{} + initiator1 := &TestMcuInitiator{ + country: "DE", + } + + ready := make(chan struct{}) + done := make(chan struct{}) + + go func() { + defer close(done) + time.Sleep(100 * time.Millisecond) + pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", StreamTypeVideo, settings1, initiator1) + require.NoError(err) + defer func() { + <-ready + pub.Close(context.Background()) + }() + }() + + listener2 := &TestMcuListener{ + id: pubId, + } + + initiator2 := &TestMcuInitiator{ + country: "DE", + } + sub, err := mcu.NewSubscriber(ctx, listener2, pubId, StreamTypeVideo, initiator2) + require.NoError(err) + defer sub.Close(context.Background()) + close(ready) + <-done +} + +func Test_JanusRemotePublisher(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + assert := assert.New(t) + require := require.New(t) + + var added atomic.Int32 + var removed atomic.Int32 + + mcu, gateway := newMcuJanusForTesting(t) + gateway.registerHandlers(map[string]TestJanusHandler{ + "add_remote_publisher": func(room *TestJanusRoom, body map[string]interface{}) (interface{}, *janus.ErrorMsg) { + assert.EqualValues(1, room.id) + if streams := body["streams"].([]interface{}); assert.Len(streams, 1) { + stream := streams[0].(map[string]interface{}) + assert.Equal("0", stream["mid"]) + assert.EqualValues(0, stream["mindex"]) + assert.Equal("audio", stream["type"]) + assert.Equal("opus", stream["codec"]) + } + added.Add(1) + return &janus.SuccessMsg{ + PluginData: janus.PluginData{ + Plugin: pluginVideoRoom, + Data: map[string]interface{}{ + "id": 12345, + "port": 10000, + "rtcp_port": 10001, + }, + }, + }, nil + }, + "remove_remote_publisher": func(room *TestJanusRoom, body map[string]interface{}) (interface{}, *janus.ErrorMsg) { + assert.EqualValues(1, room.id) + removed.Add(1) + return &janus.SuccessMsg{ + PluginData: janus.PluginData{ + Plugin: pluginVideoRoom, + Data: map[string]interface{}{}, + }, + }, nil + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + listener1 := &TestMcuListener{ + id: "publisher-id", + } + + controller := &TestMcuController{ + id: listener1.id, + } + + pub, err := mcu.NewRemotePublisher(ctx, listener1, controller, StreamTypeVideo) + require.NoError(err) + defer pub.Close(context.Background()) + + assert.EqualValues(1, added.Load()) + assert.EqualValues(0, removed.Load()) + + listener2 := &TestMcuListener{ + id: "subscriber-id", + } + + sub, err := mcu.NewRemoteSubscriber(ctx, listener2, pub) + require.NoError(err) + defer sub.Close(context.Background()) + + pub.Close(context.Background()) + + assert.EqualValues(1, added.Load()) + // The publisher is ref-counted, and still referenced by the subscriber. + assert.EqualValues(0, removed.Load()) + + sub.Close(context.Background()) + + assert.EqualValues(1, added.Load()) + assert.EqualValues(1, removed.Load()) +} diff --git a/mcu_proxy.go b/mcu_proxy.go new file mode 100644 index 0000000..0d8c537 --- /dev/null +++ b/mcu_proxy.go @@ -0,0 +1,2136 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2020 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "crypto/rsa" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "log" + "net" + "net/http" + "net/url" + "os" + "slices" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/dlintw/goconf" + "github.com/golang-jwt/jwt/v5" + "github.com/gorilla/websocket" +) + +const ( + closeTimeout = time.Second + + proxyDebugMessages = false + + // Very high value so the connections get sorted at the end. + loadNotConnected = 1000000 + + // Sort connections by load every 10 publishing requests or once per second. + connectionSortRequests = 10 + connectionSortInterval = time.Second + + proxyUrlTypeStatic = "static" + proxyUrlTypeEtcd = "etcd" + + initialWaitDelay = time.Second + maxWaitDelay = 8 * time.Second + + defaultProxyTimeoutSeconds = 2 + + rttLogDuration = 500 * time.Millisecond +) + +type McuProxy interface { + AddConnection(ignoreErrors bool, url string, ips ...net.IP) error + KeepConnection(url string, ips ...net.IP) + RemoveConnection(url string, ips ...net.IP) +} + +type mcuProxyPubSubCommon struct { + sid string + streamType StreamType + maxBitrate int + proxyId string + conn *mcuProxyConnection + listener McuListener +} + +func (c *mcuProxyPubSubCommon) Id() string { + return c.proxyId +} + +func (c *mcuProxyPubSubCommon) Sid() string { + return c.sid +} + +func (c *mcuProxyPubSubCommon) StreamType() StreamType { + return c.streamType +} + +func (c *mcuProxyPubSubCommon) MaxBitrate() int { + return c.maxBitrate +} + +func (c *mcuProxyPubSubCommon) doSendMessage(ctx context.Context, msg *ProxyClientMessage, callback func(error, map[string]interface{})) { + c.conn.performAsyncRequest(ctx, msg, func(err error, response *ProxyServerMessage) { + if err != nil { + callback(err, nil) + return + } + + if proxyDebugMessages { + log.Printf("Response from %s: %+v", c.conn, response) + } + if response.Type == "error" { + callback(response.Error, nil) + } else if response.Payload != nil { + callback(nil, response.Payload.Payload) + } else { + callback(nil, nil) + } + }) +} + +func (c *mcuProxyPubSubCommon) doProcessPayload(client McuClient, msg *PayloadProxyServerMessage) { + switch msg.Type { + case "offer": + c.listener.OnUpdateOffer(client, msg.Payload["offer"].(map[string]interface{})) + case "candidate": + c.listener.OnIceCandidate(client, msg.Payload["candidate"]) + default: + log.Printf("Unsupported payload from %s: %+v", c.conn, msg) + } +} + +type mcuProxyPublisher struct { + mcuProxyPubSubCommon + + id string + settings NewPublisherSettings +} + +func newMcuProxyPublisher(id string, sid string, streamType StreamType, maxBitrate int, settings NewPublisherSettings, proxyId string, conn *mcuProxyConnection, listener McuListener) *mcuProxyPublisher { + return &mcuProxyPublisher{ + mcuProxyPubSubCommon: mcuProxyPubSubCommon{ + sid: sid, + streamType: streamType, + maxBitrate: maxBitrate, + proxyId: proxyId, + conn: conn, + listener: listener, + }, + id: id, + settings: settings, + } +} + +func (p *mcuProxyPublisher) HasMedia(mt MediaType) bool { + return (p.settings.MediaTypes & mt) == mt +} + +func (p *mcuProxyPublisher) SetMedia(mt MediaType) { + // TODO: Also update mediaTypes on proxy. + p.settings.MediaTypes = mt +} + +func (p *mcuProxyPublisher) NotifyClosed() { + log.Printf("Publisher %s at %s was closed", p.proxyId, p.conn) + p.listener.PublisherClosed(p) + p.conn.removePublisher(p) +} + +func (p *mcuProxyPublisher) Close(ctx context.Context) { + p.NotifyClosed() + + msg := &ProxyClientMessage{ + Type: "command", + Command: &CommandProxyClientMessage{ + Type: "delete-publisher", + ClientId: p.proxyId, + }, + } + + if response, err := p.conn.performSyncRequest(ctx, msg); err != nil { + log.Printf("Could not delete publisher %s at %s: %s", p.proxyId, p.conn, err) + return + } else if response.Type == "error" { + log.Printf("Could not delete publisher %s at %s: %s", p.proxyId, p.conn, response.Error) + return + } + + log.Printf("Deleted publisher %s at %s", p.proxyId, p.conn) +} + +func (p *mcuProxyPublisher) SendMessage(ctx context.Context, message *MessageClientMessage, data *MessageClientMessageData, callback func(error, map[string]interface{})) { + msg := &ProxyClientMessage{ + Type: "payload", + Payload: &PayloadProxyClientMessage{ + Type: data.Type, + ClientId: p.proxyId, + Sid: data.Sid, + Payload: data.Payload, + }, + } + + p.doSendMessage(ctx, msg, callback) +} + +func (p *mcuProxyPublisher) ProcessPayload(msg *PayloadProxyServerMessage) { + p.doProcessPayload(p, msg) +} + +func (p *mcuProxyPublisher) ProcessEvent(msg *EventProxyServerMessage) { + switch msg.Type { + case "ice-completed": + p.listener.OnIceCompleted(p) + case "publisher-closed": + p.NotifyClosed() + default: + log.Printf("Unsupported event from %s: %+v", p.conn, msg) + } +} + +func (p *mcuProxyPublisher) GetStreams(ctx context.Context) ([]PublisherStream, error) { + return nil, errors.New("not implemented") +} + +func (p *mcuProxyPublisher) PublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { + return errors.New("remote publishing not supported for proxy publishers") +} + +func (p *mcuProxyPublisher) UnpublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { + return errors.New("remote publishing not supported for proxy publishers") +} + +type mcuProxySubscriber struct { + mcuProxyPubSubCommon + + publisherId string + publisherConn *mcuProxyConnection +} + +func newMcuProxySubscriber(publisherId string, sid string, streamType StreamType, maxBitrate int, proxyId string, conn *mcuProxyConnection, listener McuListener, publisherConn *mcuProxyConnection) *mcuProxySubscriber { + return &mcuProxySubscriber{ + mcuProxyPubSubCommon: mcuProxyPubSubCommon{ + sid: sid, + streamType: streamType, + maxBitrate: maxBitrate, + proxyId: proxyId, + conn: conn, + listener: listener, + }, + + publisherId: publisherId, + publisherConn: publisherConn, + } +} + +func (s *mcuProxySubscriber) Publisher() string { + return s.publisherId +} + +func (s *mcuProxySubscriber) NotifyClosed() { + if s.publisherConn != nil { + log.Printf("Remote subscriber %s at %s (forwarded to %s) was closed", s.proxyId, s.conn, s.publisherConn) + } else { + log.Printf("Subscriber %s at %s was closed", s.proxyId, s.conn) + } + s.listener.SubscriberClosed(s) + s.conn.removeSubscriber(s) +} + +func (s *mcuProxySubscriber) Close(ctx context.Context) { + s.NotifyClosed() + + msg := &ProxyClientMessage{ + Type: "command", + Command: &CommandProxyClientMessage{ + Type: "delete-subscriber", + ClientId: s.proxyId, + }, + } + + if response, err := s.conn.performSyncRequest(ctx, msg); err != nil { + if s.publisherConn != nil { + log.Printf("Could not delete remote subscriber %s at %s (forwarded to %s): %s", s.proxyId, s.conn, s.publisherConn, err) + } else { + log.Printf("Could not delete subscriber %s at %s: %s", s.proxyId, s.conn, err) + } + return + } else if response.Type == "error" { + if s.publisherConn != nil { + log.Printf("Could not delete remote subscriber %s at %s (forwarded to %s): %s", s.proxyId, s.conn, s.publisherConn, response.Error) + } else { + log.Printf("Could not delete subscriber %s at %s: %s", s.proxyId, s.conn, response.Error) + } + return + } + + if s.publisherConn != nil { + log.Printf("Deleted remote subscriber %s at %s (forwarded to %s)", s.proxyId, s.conn, s.publisherConn) + } else { + log.Printf("Deleted subscriber %s at %s", s.proxyId, s.conn) + } +} + +func (s *mcuProxySubscriber) SendMessage(ctx context.Context, message *MessageClientMessage, data *MessageClientMessageData, callback func(error, map[string]interface{})) { + msg := &ProxyClientMessage{ + Type: "payload", + Payload: &PayloadProxyClientMessage{ + Type: data.Type, + ClientId: s.proxyId, + Sid: data.Sid, + Payload: data.Payload, + }, + } + + s.doSendMessage(ctx, msg, callback) +} + +func (s *mcuProxySubscriber) ProcessPayload(msg *PayloadProxyServerMessage) { + s.doProcessPayload(s, msg) +} + +func (s *mcuProxySubscriber) ProcessEvent(msg *EventProxyServerMessage) { + switch msg.Type { + case "ice-completed": + s.listener.OnIceCompleted(s) + case "subscriber-sid-updated": + s.sid = msg.Sid + s.listener.SubscriberSidUpdated(s) + case "subscriber-closed": + s.NotifyClosed() + default: + log.Printf("Unsupported event from %s: %+v", s.conn, msg) + } +} + +type mcuProxyConnection struct { + proxy *mcuProxy + rawUrl string + url *url.URL + ip net.IP + + load atomic.Int64 + bandwidth atomic.Pointer[EventProxyServerBandwidth] + mu sync.Mutex + closer *Closer + closedDone *Closer + closed atomic.Bool + conn *websocket.Conn + + connectedSince time.Time + reconnectTimer *time.Timer + reconnectInterval atomic.Int64 + shutdownScheduled atomic.Bool + closeScheduled atomic.Bool + trackClose atomic.Bool + temporary atomic.Bool + + connectedNotifier SingleNotifier + + msgId atomic.Int64 + helloMsgId string + sessionId atomic.Value + country atomic.Value + + callbacks map[string]func(*ProxyServerMessage) + + publishersLock sync.RWMutex + publishers map[string]*mcuProxyPublisher + publisherIds map[string]string + + subscribersLock sync.RWMutex + subscribers map[string]*mcuProxySubscriber +} + +func newMcuProxyConnection(proxy *mcuProxy, baseUrl string, ip net.IP) (*mcuProxyConnection, error) { + parsed, err := url.Parse(baseUrl) + if err != nil { + return nil, err + } + + conn := &mcuProxyConnection{ + proxy: proxy, + rawUrl: baseUrl, + url: parsed, + ip: ip, + closer: NewCloser(), + closedDone: NewCloser(), + callbacks: make(map[string]func(*ProxyServerMessage)), + publishers: make(map[string]*mcuProxyPublisher), + publisherIds: make(map[string]string), + subscribers: make(map[string]*mcuProxySubscriber), + } + conn.reconnectInterval.Store(int64(initialReconnectInterval)) + conn.load.Store(loadNotConnected) + conn.bandwidth.Store(nil) + conn.country.Store("") + return conn, nil +} + +func (c *mcuProxyConnection) String() string { + if c.ip != nil { + return fmt.Sprintf("%s (%s)", c.rawUrl, c.ip) + } + + return c.rawUrl +} + +func (c *mcuProxyConnection) IsSameCountry(initiator McuInitiator) bool { + if initiator == nil { + return true + } + + initiatorCountry := initiator.Country() + if initiatorCountry == "" { + return true + } + + connCountry := c.Country() + if connCountry == "" { + return true + } + + return initiatorCountry == connCountry +} + +func (c *mcuProxyConnection) IsSameContinent(initiator McuInitiator) bool { + if initiator == nil { + return true + } + + initiatorCountry := initiator.Country() + if initiatorCountry == "" { + return true + } + + connCountry := c.Country() + if connCountry == "" { + return true + } + + initiatorContinents, found := ContinentMap[initiatorCountry] + if found { + m := c.proxy.getContinentsMap() + // Map continents to other continents (e.g. use Europe for Africa). + for _, continent := range initiatorContinents { + if toAdd, found := m[continent]; found { + initiatorContinents = append(initiatorContinents, toAdd...) + } + } + + } + connContinents := ContinentMap[connCountry] + return ContinentsOverlap(initiatorContinents, connContinents) +} + +type mcuProxyConnectionStats struct { + Url string `json:"url"` + IP net.IP `json:"ip,omitempty"` + Connected bool `json:"connected"` + Publishers int64 `json:"publishers"` + Clients int64 `json:"clients"` + Load *int64 `json:"load,omitempty"` + Shutdown *bool `json:"shutdown,omitempty"` + Temporary *bool `json:"temporary,omitempty"` + Uptime *time.Time `json:"uptime,omitempty"` +} + +func (c *mcuProxyConnection) GetStats() *mcuProxyConnectionStats { + result := &mcuProxyConnectionStats{ + Url: c.url.String(), + IP: c.ip, + } + c.mu.Lock() + if c.conn != nil { + result.Connected = true + result.Uptime = &c.connectedSince + load := c.Load() + result.Load = &load + shutdown := c.IsShutdownScheduled() + result.Shutdown = &shutdown + temporary := c.IsTemporary() + result.Temporary = &temporary + } + c.mu.Unlock() + c.publishersLock.RLock() + result.Publishers = int64(len(c.publishers)) + c.publishersLock.RUnlock() + c.subscribersLock.RLock() + result.Clients = int64(len(c.subscribers)) + c.subscribersLock.RUnlock() + result.Clients += result.Publishers + return result +} + +func (c *mcuProxyConnection) Load() int64 { + return c.load.Load() +} + +func (c *mcuProxyConnection) Bandwidth() *EventProxyServerBandwidth { + return c.bandwidth.Load() +} + +func (c *mcuProxyConnection) Country() string { + return c.country.Load().(string) +} + +func (c *mcuProxyConnection) SessionId() string { + sid := c.sessionId.Load() + if sid == nil { + return "" + } + + return sid.(string) +} + +func (c *mcuProxyConnection) IsConnected() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.conn != nil && c.SessionId() != "" +} + +func (c *mcuProxyConnection) IsTemporary() bool { + return c.temporary.Load() +} + +func (c *mcuProxyConnection) setTemporary() { + c.temporary.Store(true) +} + +func (c *mcuProxyConnection) clearTemporary() { + c.temporary.Store(false) +} + +func (c *mcuProxyConnection) IsShutdownScheduled() bool { + return c.shutdownScheduled.Load() || c.closeScheduled.Load() +} + +func (c *mcuProxyConnection) readPump() { + defer func() { + if !c.closed.Load() { + c.scheduleReconnect() + } else { + c.closedDone.Close() + } + }() + defer c.close() + defer func() { + c.load.Store(loadNotConnected) + c.bandwidth.Store(nil) + }() + + c.mu.Lock() + conn := c.conn + c.mu.Unlock() + + conn.SetPongHandler(func(msg string) error { + now := time.Now() + conn.SetReadDeadline(now.Add(pongWait)) // nolint + if msg == "" { + return nil + } + if ts, err := strconv.ParseInt(msg, 10, 64); err == nil { + rtt := now.Sub(time.Unix(0, ts)) + if rtt >= rttLogDuration { + rtt_ms := rtt.Nanoseconds() / time.Millisecond.Nanoseconds() + log.Printf("Proxy at %s has RTT of %d ms (%s)", c, rtt_ms, rtt) + } + } + return nil + }) + + for { + conn.SetReadDeadline(time.Now().Add(pongWait)) // nolint + _, message, err := conn.ReadMessage() + if err != nil { + if errors.Is(err, websocket.ErrCloseSent) { + break + } else if _, ok := err.(*websocket.CloseError); !ok || websocket.IsUnexpectedCloseError(err, + websocket.CloseNormalClosure, + websocket.CloseGoingAway, + websocket.CloseNoStatusReceived) { + log.Printf("Error reading from %s: %v", c, err) + } + break + } + + var msg ProxyServerMessage + if err := json.Unmarshal(message, &msg); err != nil { + log.Printf("Error unmarshaling %s from %s: %s", string(message), c, err) + continue + } + + c.processMessage(&msg) + } +} + +func (c *mcuProxyConnection) sendPing() bool { + c.mu.Lock() + defer c.mu.Unlock() + if c.conn == nil { + return false + } + + now := time.Now() + msg := strconv.FormatInt(now.UnixNano(), 10) + c.conn.SetWriteDeadline(now.Add(writeWait)) // nolint + if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil { + log.Printf("Could not send ping to proxy at %s: %v", c, err) + go c.scheduleReconnect() + return false + } + + return true +} + +func (c *mcuProxyConnection) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + }() + + c.reconnectTimer = time.NewTimer(0) + defer c.reconnectTimer.Stop() + for { + select { + case <-c.reconnectTimer.C: + c.reconnect() + case <-ticker.C: + c.sendPing() + case <-c.closer.C: + return + } + } +} + +func (c *mcuProxyConnection) start() { + go c.writePump() +} + +func (c *mcuProxyConnection) sendClose() error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.conn == nil { + return ErrNotConnected + } + + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint + return c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) +} + +func (c *mcuProxyConnection) stop(ctx context.Context) { + if !c.closed.CompareAndSwap(false, true) { + return + } + + c.closer.Close() + if err := c.sendClose(); err != nil { + if err != ErrNotConnected { + log.Printf("Could not send close message to %s: %s", c, err) + } + c.close() + return + } + + select { + case <-c.closedDone.C: + case <-ctx.Done(): + if err := ctx.Err(); err != nil { + log.Printf("Error waiting for connection to %s get closed: %s", c, err) + c.close() + } + } +} + +func (c *mcuProxyConnection) close() { + c.mu.Lock() + defer c.mu.Unlock() + + c.connectedNotifier.Reset() + + if c.conn != nil { + c.conn.Close() + c.conn = nil + if c.trackClose.CompareAndSwap(true, false) { + statsConnectedProxyBackendsCurrent.WithLabelValues(c.Country()).Dec() + } + } +} + +func (c *mcuProxyConnection) stopCloseIfEmpty() { + c.closeScheduled.Store(false) +} + +func (c *mcuProxyConnection) closeIfEmpty() bool { + c.closeScheduled.Store(true) + + var total int64 + c.publishersLock.RLock() + total += int64(len(c.publishers)) + c.publishersLock.RUnlock() + c.subscribersLock.RLock() + total += int64(len(c.subscribers)) + c.subscribersLock.RUnlock() + if total > 0 { + // Connection will be closed once all clients have disconnected. + log.Printf("Connection to %s is still used by %d clients, defer closing", c, total) + return false + } + + go func() { + ctx, cancel := context.WithTimeout(context.Background(), closeTimeout) + defer cancel() + + log.Printf("All clients disconnected, closing connection to %s", c) + c.stop(ctx) + + c.proxy.removeConnection(c) + }() + return true +} + +func (c *mcuProxyConnection) scheduleReconnect() { + if err := c.sendClose(); err != nil && err != ErrNotConnected { + log.Printf("Could not send close message to %s: %s", c, err) + } + c.close() + + if c.IsShutdownScheduled() { + c.proxy.removeConnection(c) + return + } + + interval := c.reconnectInterval.Load() + c.reconnectTimer.Reset(time.Duration(interval)) + + interval = interval * 2 + if interval > int64(maxReconnectInterval) { + interval = int64(maxReconnectInterval) + } + c.reconnectInterval.Store(interval) +} + +func (c *mcuProxyConnection) reconnect() { + u, err := c.url.Parse("proxy") + if err != nil { + log.Printf("Could not resolve url to proxy at %s: %s", c, err) + c.scheduleReconnect() + return + } + if u.Scheme == "http" { + u.Scheme = "ws" + } else if u.Scheme == "https" { + u.Scheme = "wss" + } + + dialer := c.proxy.dialer + if c.ip != nil { + dialer = &websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: c.proxy.dialer.HandshakeTimeout, + TLSClientConfig: c.proxy.dialer.TLSClientConfig, + + // Override DNS lookup and connect to custom IP address. + NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if _, port, err := net.SplitHostPort(addr); err == nil { + addr = net.JoinHostPort(c.ip.String(), port) + } + + return net.Dial(network, addr) + }, + } + } + conn, _, err := dialer.Dial(u.String(), nil) + if err != nil { + log.Printf("Could not connect to %s: %s", c, err) + c.scheduleReconnect() + return + } + + if c.IsShutdownScheduled() { + c.proxy.removeConnection(c) + return + } + + log.Printf("Connected to %s", c) + c.closed.Store(false) + + c.mu.Lock() + c.connectedSince = time.Now() + c.conn = conn + c.mu.Unlock() + + c.reconnectInterval.Store(int64(initialReconnectInterval)) + c.shutdownScheduled.Store(false) + if err := c.sendHello(); err != nil { + log.Printf("Could not send hello request to %s: %s", c, err) + c.scheduleReconnect() + return + } + + if !c.sendPing() { + return + } + + go c.readPump() +} + +func (c *mcuProxyConnection) waitUntilConnected(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.conn != nil { + return nil + } + + waiter := c.connectedNotifier.NewWaiter() + defer c.connectedNotifier.Release(waiter) + + c.mu.Unlock() + defer c.mu.Lock() + return waiter.Wait(ctx) +} + +func (c *mcuProxyConnection) removePublisher(publisher *mcuProxyPublisher) { + c.proxy.removePublisher(publisher) + + c.publishersLock.Lock() + defer c.publishersLock.Unlock() + + if _, found := c.publishers[publisher.proxyId]; found { + delete(c.publishers, publisher.proxyId) + statsPublishersCurrent.WithLabelValues(string(publisher.StreamType())).Dec() + } + delete(c.publisherIds, getStreamId(publisher.id, publisher.StreamType())) + + if len(c.publishers) == 0 && (c.closeScheduled.Load() || c.IsTemporary()) { + go c.closeIfEmpty() + } +} + +func (c *mcuProxyConnection) clearPublishers() { + c.publishersLock.Lock() + defer c.publishersLock.Unlock() + + go func(publishers map[string]*mcuProxyPublisher) { + for _, publisher := range publishers { + publisher.NotifyClosed() + } + }(c.publishers) + // Can't use clear(...) here as the map is processed by the goroutine above. + c.publishers = make(map[string]*mcuProxyPublisher) + clear(c.publisherIds) + + if c.closeScheduled.Load() || c.IsTemporary() { + go c.closeIfEmpty() + } +} + +func (c *mcuProxyConnection) removeSubscriber(subscriber *mcuProxySubscriber) { + c.subscribersLock.Lock() + defer c.subscribersLock.Unlock() + + if _, found := c.subscribers[subscriber.proxyId]; found { + delete(c.subscribers, subscriber.proxyId) + statsSubscribersCurrent.WithLabelValues(string(subscriber.StreamType())).Dec() + } + + if len(c.subscribers) == 0 && (c.closeScheduled.Load() || c.IsTemporary()) { + go c.closeIfEmpty() + } +} + +func (c *mcuProxyConnection) clearSubscribers() { + c.subscribersLock.Lock() + defer c.subscribersLock.Unlock() + + go func(subscribers map[string]*mcuProxySubscriber) { + for _, subscriber := range subscribers { + subscriber.NotifyClosed() + } + }(c.subscribers) + // Can't use clear(...) here as the map is processed by the goroutine above. + c.subscribers = make(map[string]*mcuProxySubscriber) + + if c.closeScheduled.Load() || c.IsTemporary() { + go c.closeIfEmpty() + } +} + +func (c *mcuProxyConnection) clearCallbacks() { + c.mu.Lock() + defer c.mu.Unlock() + + clear(c.callbacks) +} + +func (c *mcuProxyConnection) getCallback(id string) func(*ProxyServerMessage) { + c.mu.Lock() + defer c.mu.Unlock() + + callback, found := c.callbacks[id] + if found { + delete(c.callbacks, id) + } + return callback +} + +func (c *mcuProxyConnection) processMessage(msg *ProxyServerMessage) { + if c.helloMsgId != "" && msg.Id == c.helloMsgId { + c.helloMsgId = "" + switch msg.Type { + case "error": + if msg.Error.Code == "no_such_session" { + log.Printf("Session %s could not be resumed on %s, registering new", c.SessionId(), c) + c.clearPublishers() + c.clearSubscribers() + c.clearCallbacks() + c.sessionId.Store("") + if err := c.sendHello(); err != nil { + log.Printf("Could not send hello request to %s: %s", c, err) + c.scheduleReconnect() + } + return + } + + log.Printf("Hello connection to %s failed with %+v, reconnecting", c, msg.Error) + c.scheduleReconnect() + case "hello": + resumed := c.SessionId() == msg.Hello.SessionId + c.sessionId.Store(msg.Hello.SessionId) + country := "" + if msg.Hello.Server != nil { + if country = msg.Hello.Server.Country; country != "" && !IsValidCountry(country) { + log.Printf("Proxy %s sent invalid country %s in hello response", c, country) + country = "" + } + } + c.country.Store(country) + if resumed { + log.Printf("Resumed session %s on %s", c.SessionId(), c) + } else if country != "" { + log.Printf("Received session %s from %s (in %s)", c.SessionId(), c, country) + } else { + log.Printf("Received session %s from %s", c.SessionId(), c) + } + if c.trackClose.CompareAndSwap(false, true) { + statsConnectedProxyBackendsCurrent.WithLabelValues(c.Country()).Inc() + } + + c.connectedNotifier.Notify() + default: + log.Printf("Received unsupported hello response %+v from %s, reconnecting", msg, c) + c.scheduleReconnect() + } + return + } + + if proxyDebugMessages { + log.Printf("Received from %s: %+v", c, msg) + } + callback := c.getCallback(msg.Id) + if callback != nil { + callback(msg) + return + } + + switch msg.Type { + case "payload": + c.processPayload(msg) + case "event": + c.processEvent(msg) + case "bye": + c.processBye(msg) + default: + log.Printf("Unsupported message received from %s: %+v", c, msg) + } +} + +func (c *mcuProxyConnection) processPayload(msg *ProxyServerMessage) { + payload := msg.Payload + c.publishersLock.RLock() + publisher, found := c.publishers[payload.ClientId] + c.publishersLock.RUnlock() + if found { + publisher.ProcessPayload(payload) + return + } + + c.subscribersLock.RLock() + subscriber, found := c.subscribers[payload.ClientId] + c.subscribersLock.RUnlock() + if found { + subscriber.ProcessPayload(payload) + return + } + + log.Printf("Received payload for unknown client %+v from %s", payload, c) +} + +func (c *mcuProxyConnection) processEvent(msg *ProxyServerMessage) { + event := msg.Event + switch event.Type { + case "backend-disconnected": + log.Printf("Upstream backend at %s got disconnected, reset MCU objects", c) + c.clearPublishers() + c.clearSubscribers() + c.clearCallbacks() + // TODO: Should we also reconnect? + return + case "backend-connected": + log.Printf("Upstream backend at %s is connected", c) + return + case "update-load": + if proxyDebugMessages { + log.Printf("Load of %s now at %d (%s)", c, event.Load, event.Bandwidth) + } + c.load.Store(event.Load) + c.bandwidth.Store(event.Bandwidth) + statsProxyBackendLoadCurrent.WithLabelValues(c.url.String()).Set(float64(event.Load)) + return + case "shutdown-scheduled": + log.Printf("Proxy %s is scheduled to shutdown", c) + c.shutdownScheduled.Store(true) + return + } + + if proxyDebugMessages { + log.Printf("Process event from %s: %+v", c, event) + } + c.publishersLock.RLock() + publisher, found := c.publishers[event.ClientId] + c.publishersLock.RUnlock() + if found { + publisher.ProcessEvent(event) + return + } + + c.subscribersLock.RLock() + subscriber, found := c.subscribers[event.ClientId] + c.subscribersLock.RUnlock() + if found { + subscriber.ProcessEvent(event) + return + } + + log.Printf("Received event for unknown client %+v from %s", event, c) +} + +func (c *mcuProxyConnection) processBye(msg *ProxyServerMessage) { + bye := msg.Bye + switch bye.Reason { + case "session_resumed": + log.Printf("Session %s on %s was resumed by other client, resetting", c.SessionId(), c) + c.sessionId.Store("") + case "session_expired": + log.Printf("Session %s expired on %s, resetting", c.SessionId(), c) + c.sessionId.Store("") + case "session_closed": + log.Printf("Session %s was closed on %s, resetting", c.SessionId(), c) + c.sessionId.Store("") + default: + log.Printf("Received bye with unsupported reason from %s %+v", c, bye) + } +} + +func (c *mcuProxyConnection) sendHello() error { + c.helloMsgId = strconv.FormatInt(c.msgId.Add(1), 10) + msg := &ProxyClientMessage{ + Id: c.helloMsgId, + Type: "hello", + Hello: &HelloProxyClientMessage{ + Version: "1.0", + }, + } + if sessionId := c.SessionId(); sessionId != "" { + msg.Hello.ResumeId = sessionId + } else { + tokenString, err := c.proxy.createToken("") + if err != nil { + return err + } + + msg.Hello.Token = tokenString + } + return c.sendMessage(msg) +} + +func (c *mcuProxyConnection) sendMessage(msg *ProxyClientMessage) error { + c.mu.Lock() + defer c.mu.Unlock() + + return c.sendMessageLocked(msg) +} + +func (c *mcuProxyConnection) sendMessageLocked(msg *ProxyClientMessage) error { + if proxyDebugMessages { + log.Printf("Send message to %s: %+v", c, msg) + } + if c.conn == nil { + return ErrNotConnected + } + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint + return c.conn.WriteJSON(msg) +} + +func (c *mcuProxyConnection) performAsyncRequest(ctx context.Context, msg *ProxyClientMessage, callback func(err error, response *ProxyServerMessage)) { + msgId := strconv.FormatInt(c.msgId.Add(1), 10) + msg.Id = msgId + + c.mu.Lock() + defer c.mu.Unlock() + c.callbacks[msgId] = func(msg *ProxyServerMessage) { + callback(nil, msg) + } + if err := c.sendMessageLocked(msg); err != nil { + delete(c.callbacks, msgId) + go callback(err, nil) + return + } +} + +func (c *mcuProxyConnection) performSyncRequest(ctx context.Context, msg *ProxyClientMessage) (*ProxyServerMessage, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + errChan := make(chan error, 1) + responseChan := make(chan *ProxyServerMessage, 1) + c.performAsyncRequest(ctx, msg, func(err error, response *ProxyServerMessage) { + if err != nil { + errChan <- err + } else { + responseChan <- response + } + }) + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err := <-errChan: + return nil, err + case response := <-responseChan: + return response, nil + } +} + +func (c *mcuProxyConnection) newPublisher(ctx context.Context, listener McuListener, id string, sid string, streamType StreamType, settings NewPublisherSettings) (McuPublisher, error) { + msg := &ProxyClientMessage{ + Type: "command", + Command: &CommandProxyClientMessage{ + Type: "create-publisher", + Sid: sid, + StreamType: streamType, + PublisherSettings: &settings, + // Include for older version of the signaling proxy. + Bitrate: settings.Bitrate, + MediaTypes: settings.MediaTypes, + }, + } + + response, err := c.performSyncRequest(ctx, msg) + if err != nil { + // TODO: Cancel request + return nil, err + } else if response.Type == "error" { + return nil, fmt.Errorf("Error creating %s publisher for %s on %s: %+v", streamType, id, c, response.Error) + } + + proxyId := response.Command.Id + log.Printf("Created %s publisher %s on %s for %s", streamType, proxyId, c, id) + publisher := newMcuProxyPublisher(id, sid, streamType, response.Command.Bitrate, settings, proxyId, c, listener) + c.publishersLock.Lock() + c.publishers[proxyId] = publisher + c.publisherIds[getStreamId(id, streamType)] = proxyId + c.publishersLock.Unlock() + statsPublishersCurrent.WithLabelValues(string(streamType)).Inc() + statsPublishersTotal.WithLabelValues(string(streamType)).Inc() + return publisher, nil +} + +func (c *mcuProxyConnection) newSubscriber(ctx context.Context, listener McuListener, publisherId string, publisherSessionId string, streamType StreamType) (McuSubscriber, error) { + msg := &ProxyClientMessage{ + Type: "command", + Command: &CommandProxyClientMessage{ + Type: "create-subscriber", + StreamType: streamType, + PublisherId: publisherId, + }, + } + + response, err := c.performSyncRequest(ctx, msg) + if err != nil { + // TODO: Cancel request + return nil, err + } else if response.Type == "error" { + return nil, fmt.Errorf("Error creating %s subscriber for %s on %s: %+v", streamType, publisherSessionId, c, response.Error) + } + + proxyId := response.Command.Id + log.Printf("Created %s subscriber %s on %s for %s", streamType, proxyId, c, publisherSessionId) + subscriber := newMcuProxySubscriber(publisherSessionId, response.Command.Sid, streamType, response.Command.Bitrate, proxyId, c, listener, nil) + c.subscribersLock.Lock() + c.subscribers[proxyId] = subscriber + c.subscribersLock.Unlock() + statsSubscribersCurrent.WithLabelValues(string(streamType)).Inc() + statsSubscribersTotal.WithLabelValues(string(streamType)).Inc() + return subscriber, nil +} + +func (c *mcuProxyConnection) newRemoteSubscriber(ctx context.Context, listener McuListener, publisherId string, publisherSessionId string, streamType StreamType, publisherConn *mcuProxyConnection) (McuSubscriber, error) { + if c == publisherConn { + return c.newSubscriber(ctx, listener, publisherId, publisherSessionId, streamType) + } + + remoteToken, err := c.proxy.createToken(publisherId) + if err != nil { + return nil, err + } + + msg := &ProxyClientMessage{ + Type: "command", + Command: &CommandProxyClientMessage{ + Type: "create-subscriber", + StreamType: streamType, + PublisherId: publisherId, + + RemoteUrl: publisherConn.rawUrl, + RemoteToken: remoteToken, + }, + } + + response, err := c.performSyncRequest(ctx, msg) + if err != nil { + // TODO: Cancel request + return nil, err + } else if response.Type == "error" { + return nil, fmt.Errorf("Error creating remote %s subscriber for %s on %s (forwarded to %s): %+v", streamType, publisherSessionId, c, publisherConn, response.Error) + } + + proxyId := response.Command.Id + log.Printf("Created remote %s subscriber %s on %s for %s (forwarded to %s)", streamType, proxyId, c, publisherSessionId, publisherConn) + subscriber := newMcuProxySubscriber(publisherSessionId, response.Command.Sid, streamType, response.Command.Bitrate, proxyId, c, listener, publisherConn) + c.subscribersLock.Lock() + c.subscribers[proxyId] = subscriber + c.subscribersLock.Unlock() + statsSubscribersCurrent.WithLabelValues(string(streamType)).Inc() + statsSubscribersTotal.WithLabelValues(string(streamType)).Inc() + return subscriber, nil +} + +type mcuProxySettings struct { + mcuCommonSettings +} + +func newMcuProxySettings(config *goconf.ConfigFile) (McuSettings, error) { + settings := &mcuProxySettings{} + if err := settings.load(config); err != nil { + return nil, err + } + + return settings, nil +} + +func (s *mcuProxySettings) load(config *goconf.ConfigFile) error { + if err := s.mcuCommonSettings.load(config); err != nil { + return err + } + + proxyTimeoutSeconds, _ := config.GetInt("mcu", "proxytimeout") + if proxyTimeoutSeconds <= 0 { + proxyTimeoutSeconds = defaultProxyTimeoutSeconds + } + proxyTimeout := time.Duration(proxyTimeoutSeconds) * time.Second + log.Printf("Using a timeout of %s for proxy requests", proxyTimeout) + s.setTimeout(proxyTimeout) + return nil +} + +func (s *mcuProxySettings) Reload(config *goconf.ConfigFile) { + if err := s.load(config); err != nil { + log.Printf("Error reloading proxy settings: %s", err) + } +} + +type mcuProxy struct { + urlType string + tokenId string + tokenKey *rsa.PrivateKey + config ProxyConfig + + dialer *websocket.Dialer + connections []*mcuProxyConnection + connectionsMap map[string][]*mcuProxyConnection + connectionsMu sync.RWMutex + connRequests atomic.Int64 + nextSort atomic.Int64 + + settings McuSettings + + mu sync.RWMutex + publishers map[string]*mcuProxyConnection + + publisherWaiters ChannelWaiters + + continentsMap atomic.Value + + rpcClients *GrpcClients +} + +func NewMcuProxy(config *goconf.ConfigFile, etcdClient *EtcdClient, rpcClients *GrpcClients, dnsMonitor *DnsMonitor) (Mcu, error) { + urlType, _ := config.GetString("mcu", "urltype") + if urlType == "" { + urlType = proxyUrlTypeStatic + } + + tokenId, _ := config.GetString("mcu", "token_id") + if tokenId == "" { + return nil, fmt.Errorf("No token id configured") + } + tokenKeyFilename, _ := config.GetString("mcu", "token_key") + if tokenKeyFilename == "" { + return nil, fmt.Errorf("No token key configured") + } + tokenKeyData, err := os.ReadFile(tokenKeyFilename) + if err != nil { + return nil, fmt.Errorf("Could not read private key from %s: %s", tokenKeyFilename, err) + } + tokenKey, err := jwt.ParseRSAPrivateKeyFromPEM(tokenKeyData) + if err != nil { + return nil, fmt.Errorf("Could not parse private key from %s: %s", tokenKeyFilename, err) + } + + settings, err := newMcuProxySettings((config)) + if err != nil { + return nil, err + } + + mcu := &mcuProxy{ + urlType: urlType, + tokenId: tokenId, + tokenKey: tokenKey, + + dialer: &websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: settings.Timeout(), + }, + connectionsMap: make(map[string][]*mcuProxyConnection), + settings: settings, + + publishers: make(map[string]*mcuProxyConnection), + + rpcClients: rpcClients, + } + + if err := mcu.loadContinentsMap(config); err != nil { + return nil, err + } + + skipverify, _ := config.GetBool("mcu", "skipverify") + if skipverify { + log.Println("WARNING: MCU verification is disabled!") + mcu.dialer.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: skipverify, + } + } + + switch urlType { + case proxyUrlTypeStatic: + mcu.config, err = NewProxyConfigStatic(config, mcu, dnsMonitor) + case proxyUrlTypeEtcd: + mcu.config, err = NewProxyConfigEtcd(config, etcdClient, mcu) + default: + err = fmt.Errorf("Unsupported proxy URL type %s", urlType) + } + if err != nil { + return nil, err + } + + return mcu, nil +} + +func (m *mcuProxy) loadContinentsMap(config *goconf.ConfigFile) error { + options, err := GetStringOptions(config, "continent-overrides", false) + if err != nil { + return err + } + + if len(options) == 0 { + m.setContinentsMap(nil) + return nil + } + + continentsMap := make(map[string][]string) + for option, value := range options { + option = strings.ToUpper(strings.TrimSpace(option)) + if !IsValidContinent(option) { + log.Printf("Ignore unknown continent %s", option) + continue + } + + var values []string + for _, v := range strings.Split(value, ",") { + v = strings.ToUpper(strings.TrimSpace(v)) + if !IsValidContinent(v) { + log.Printf("Ignore unknown continent %s for override %s", v, option) + continue + } + values = append(values, v) + } + if len(values) == 0 { + log.Printf("No valid values found for continent override %s, ignoring", option) + continue + } + + continentsMap[option] = values + log.Printf("Mapping users on continent %s to %s", option, values) + } + + m.setContinentsMap(continentsMap) + return nil +} + +func (m *mcuProxy) Start(ctx context.Context) error { + return m.config.Start() +} + +func (m *mcuProxy) Stop() { + m.connectionsMu.RLock() + defer m.connectionsMu.RUnlock() + + ctx, cancel := context.WithTimeout(context.Background(), closeTimeout) + defer cancel() + for _, c := range m.connections { + c.stop(ctx) + } + + m.config.Stop() +} + +func (m *mcuProxy) createToken(subject string) (string, error) { + claims := &TokenClaims{ + jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(time.Now()), + Issuer: m.tokenId, + Subject: subject, + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + tokenString, err := token.SignedString(m.tokenKey) + if err != nil { + return "", err + } + + return tokenString, nil +} + +func (m *mcuProxy) hasConnections() bool { + m.connectionsMu.RLock() + defer m.connectionsMu.RUnlock() + for _, conn := range m.connections { + if conn.IsConnected() { + return true + } + } + return false +} + +func (m *mcuProxy) WaitForConnections(ctx context.Context) error { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + for !m.hasConnections() { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } + return nil +} + +func (m *mcuProxy) AddConnection(ignoreErrors bool, url string, ips ...net.IP) error { + m.connectionsMu.Lock() + defer m.connectionsMu.Unlock() + + var conns []*mcuProxyConnection + if len(ips) == 0 { + conn, err := newMcuProxyConnection(m, url, nil) + if err != nil { + if ignoreErrors { + log.Printf("Could not create proxy connection to %s: %s", url, err) + return nil + } + + return err + } + + conns = append(conns, conn) + } else { + for _, ip := range ips { + conn, err := newMcuProxyConnection(m, url, ip) + if err != nil { + if ignoreErrors { + log.Printf("Could not create proxy connection to %s (%s): %s", url, ip, err) + continue + } + + return err + } + + conns = append(conns, conn) + } + } + + for _, conn := range conns { + log.Printf("Adding new connection to %s", conn) + conn.start() + + m.connections = append(m.connections, conn) + if existing, found := m.connectionsMap[url]; found { + m.connectionsMap[url] = append(existing, conn) + } else { + m.connectionsMap[url] = []*mcuProxyConnection{conn} + } + } + + m.nextSort.Store(0) + return nil +} + +func containsIP(ips []net.IP, ip net.IP) bool { + for _, i := range ips { + if i.Equal(ip) { + return true + } + } + + return false +} + +func (m *mcuProxy) iterateConnections(url string, ips []net.IP, f func(conn *mcuProxyConnection)) { + m.connectionsMu.Lock() + defer m.connectionsMu.Unlock() + + conns, found := m.connectionsMap[url] + if !found { + return + } + + var toRemove []*mcuProxyConnection + if len(ips) == 0 { + toRemove = conns + } else { + for _, conn := range conns { + if containsIP(ips, conn.ip) { + toRemove = append(toRemove, conn) + } + } + } + + for _, conn := range toRemove { + f(conn) + } +} + +func (m *mcuProxy) RemoveConnection(url string, ips ...net.IP) { + m.iterateConnections(url, ips, func(conn *mcuProxyConnection) { + log.Printf("Removing connection to %s", conn) + conn.closeIfEmpty() + }) +} + +func (m *mcuProxy) KeepConnection(url string, ips ...net.IP) { + m.iterateConnections(url, ips, func(conn *mcuProxyConnection) { + conn.stopCloseIfEmpty() + conn.clearTemporary() + }) +} + +func (m *mcuProxy) Reload(config *goconf.ConfigFile) { + m.settings.Reload(config) + + if m.settings.Timeout() != m.dialer.HandshakeTimeout { + m.dialer.HandshakeTimeout = m.settings.Timeout() + } + + if err := m.loadContinentsMap(config); err != nil { + log.Printf("Error loading continents map: %s", err) + } + + if err := m.config.Reload(config); err != nil { + log.Printf("could not reload proxy configuration: %s", err) + } +} + +func (m *mcuProxy) removeConnection(c *mcuProxyConnection) { + m.connectionsMu.Lock() + defer m.connectionsMu.Unlock() + + if conns, found := m.connectionsMap[c.rawUrl]; found { + for idx, conn := range conns { + if conn == c { + conns = append(conns[:idx], conns[idx+1:]...) + break + } + } + if len(conns) == 0 { + delete(m.connectionsMap, c.rawUrl) + m.connections = nil + for _, conns := range m.connectionsMap { + m.connections = append(m.connections, conns...) + } + } else { + m.connectionsMap[c.rawUrl] = conns + } + + m.nextSort.Store(0) + } +} + +func (m *mcuProxy) SetOnConnected(f func()) { + // Not supported. +} + +func (m *mcuProxy) SetOnDisconnected(f func()) { + // Not supported. +} + +type mcuProxyStats struct { + Publishers int64 `json:"publishers"` + Clients int64 `json:"clients"` + Details []*mcuProxyConnectionStats `json:"details"` +} + +func (m *mcuProxy) GetStats() interface{} { + result := &mcuProxyStats{} + + m.connectionsMu.RLock() + defer m.connectionsMu.RUnlock() + + for _, conn := range m.connections { + stats := conn.GetStats() + result.Publishers += stats.Publishers + result.Clients += stats.Clients + result.Details = append(result.Details, stats) + } + return result +} + +func (m *mcuProxy) getContinentsMap() map[string][]string { + continentsMap := m.continentsMap.Load() + if continentsMap == nil { + return nil + } + return continentsMap.(map[string][]string) +} + +func (m *mcuProxy) setContinentsMap(continentsMap map[string][]string) { + if continentsMap == nil { + continentsMap = make(map[string][]string) + } + m.continentsMap.Store(continentsMap) +} + +type mcuProxyConnectionsList []*mcuProxyConnection + +func (l mcuProxyConnectionsList) Len() int { + return len(l) +} + +func (l mcuProxyConnectionsList) Less(i, j int) bool { + return l[i].Load() < l[j].Load() +} + +func (l mcuProxyConnectionsList) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func (l mcuProxyConnectionsList) Sort() { + sort.Sort(l) +} + +func ContinentsOverlap(a, b []string) bool { + if len(a) == 0 || len(b) == 0 { + return false + } + + for _, checkA := range a { + for _, checkB := range b { + if checkA == checkB { + return true + } + } + } + return false +} + +func sortConnectionsForCountry(connections []*mcuProxyConnection, country string, continentMap map[string][]string) []*mcuProxyConnection { + // Move connections in the same country to the start of the list. + sorted := make(mcuProxyConnectionsList, 0, len(connections)) + unprocessed := make(mcuProxyConnectionsList, 0, len(connections)) + for _, conn := range connections { + if country == conn.Country() { + sorted = append(sorted, conn) + } else { + unprocessed = append(unprocessed, conn) + } + } + if continents, found := ContinentMap[country]; found && len(unprocessed) > 1 { + remaining := make(mcuProxyConnectionsList, 0, len(unprocessed)) + // Map continents to other continents (e.g. use Europe for Africa). + for _, continent := range continents { + if toAdd, found := continentMap[continent]; found { + continents = append(continents, toAdd...) + } + } + + // Next up are connections on the same or mapped continent. + for _, conn := range unprocessed { + connCountry := conn.Country() + if IsValidCountry(connCountry) { + connContinents := ContinentMap[connCountry] + if ContinentsOverlap(continents, connContinents) { + sorted = append(sorted, conn) + } else { + remaining = append(remaining, conn) + } + } else { + remaining = append(remaining, conn) + } + } + unprocessed = remaining + } + // Add all other connections by load. + sorted = append(sorted, unprocessed...) + return sorted +} + +func (m *mcuProxy) getSortedConnections(initiator McuInitiator) []*mcuProxyConnection { + m.connectionsMu.RLock() + connections := m.connections + m.connectionsMu.RUnlock() + if len(connections) < 2 { + return connections + } + + // Connections are re-sorted every requests or + // every . + now := time.Now().UnixNano() + if m.connRequests.Add(1)%connectionSortRequests == 0 || m.nextSort.Load() <= now { + m.nextSort.Store(now + int64(connectionSortInterval)) + + sorted := make(mcuProxyConnectionsList, len(connections)) + copy(sorted, connections) + + sorted.Sort() + + m.connectionsMu.Lock() + m.connections = sorted + m.connectionsMu.Unlock() + connections = sorted + } + + if initiator != nil { + if country := initiator.Country(); IsValidCountry(country) { + connections = sortConnectionsForCountry(connections, country, m.getContinentsMap()) + } + } + return connections +} + +func (m *mcuProxy) removePublisher(publisher *mcuProxyPublisher) { + m.mu.Lock() + defer m.mu.Unlock() + + delete(m.publishers, getStreamId(publisher.id, publisher.StreamType())) +} + +func (m *mcuProxy) createPublisher(ctx context.Context, listener McuListener, id string, sid string, streamType StreamType, settings NewPublisherSettings, initiator McuInitiator, connections []*mcuProxyConnection, isAllowed func(c *mcuProxyConnection) bool) McuPublisher { + var maxBitrate int + if streamType == StreamTypeScreen { + maxBitrate = int(m.settings.MaxScreenBitrate()) + } else { + maxBitrate = int(m.settings.MaxStreamBitrate()) + } + + publisherSettings := settings + if publisherSettings.Bitrate <= 0 { + publisherSettings.Bitrate = maxBitrate + } else { + publisherSettings.Bitrate = min(publisherSettings.Bitrate, maxBitrate) + } + + for _, conn := range connections { + if !isAllowed(conn) || conn.IsShutdownScheduled() || conn.IsTemporary() { + continue + } + + subctx, cancel := context.WithTimeout(ctx, m.settings.Timeout()) + defer cancel() + + publisher, err := conn.newPublisher(subctx, listener, id, sid, streamType, publisherSettings) + if err != nil { + log.Printf("Could not create %s publisher for %s on %s: %s", streamType, id, conn, err) + continue + } + + m.mu.Lock() + m.publishers[getStreamId(id, streamType)] = conn + m.mu.Unlock() + m.publisherWaiters.Wakeup() + return publisher + } + + return nil +} + +func (m *mcuProxy) NewPublisher(ctx context.Context, listener McuListener, id string, sid string, streamType StreamType, settings NewPublisherSettings, initiator McuInitiator) (McuPublisher, error) { + connections := m.getSortedConnections(initiator) + publisher := m.createPublisher(ctx, listener, id, sid, streamType, settings, initiator, connections, func(c *mcuProxyConnection) bool { + bw := c.Bandwidth() + return bw == nil || bw.AllowIncoming() + }) + if publisher == nil { + // No proxy has available bandwidth, select one with the lowest currently used bandwidth. + connections2 := make([]*mcuProxyConnection, 0, len(connections)) + for _, c := range connections { + if c.Bandwidth() != nil { + connections2 = append(connections2, c) + } + } + slices.SortFunc(connections2, func(a *mcuProxyConnection, b *mcuProxyConnection) int { + var incoming_a *float64 + if bw := a.Bandwidth(); bw != nil { + incoming_a = bw.Incoming + } + + var incoming_b *float64 + if bw := b.Bandwidth(); bw != nil { + incoming_b = bw.Incoming + } + + if incoming_a == nil && incoming_b == nil { + return 0 + } else if incoming_a == nil && incoming_b != nil { + return -1 + } else if incoming_a != nil && incoming_b == nil { + return -1 + } else if *incoming_a < *incoming_b { + return -1 + } else if *incoming_a > *incoming_b { + return 1 + } + return 0 + }) + publisher = m.createPublisher(ctx, listener, id, sid, streamType, settings, initiator, connections2, func(c *mcuProxyConnection) bool { + return true + }) + } + + if publisher == nil { + statsProxyNobackendAvailableTotal.WithLabelValues(string(streamType)).Inc() + return nil, fmt.Errorf("No MCU connection available") + } + + return publisher, nil +} + +func (m *mcuProxy) getPublisherConnection(publisher string, streamType StreamType) *mcuProxyConnection { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.publishers[getStreamId(publisher, streamType)] +} + +func (m *mcuProxy) waitForPublisherConnection(ctx context.Context, publisher string, streamType StreamType) *mcuProxyConnection { + m.mu.Lock() + defer m.mu.Unlock() + + conn := m.publishers[getStreamId(publisher, streamType)] + if conn != nil { + // Publisher was created while waiting for lock. + return conn + } + + ch := make(chan struct{}, 1) + id := m.publisherWaiters.Add(ch) + defer m.publisherWaiters.Remove(id) + + statsWaitingForPublisherTotal.WithLabelValues(string(streamType)).Inc() + for { + m.mu.Unlock() + select { + case <-ch: + m.mu.Lock() + conn = m.publishers[getStreamId(publisher, streamType)] + if conn != nil { + return conn + } + case <-ctx.Done(): + m.mu.Lock() + return nil + } + } +} + +type proxyPublisherInfo struct { + id string + conn *mcuProxyConnection + err error +} + +func (m *mcuProxy) createSubscriber(ctx context.Context, listener McuListener, id string, publisher string, streamType StreamType, publisherConn *mcuProxyConnection, connections []*mcuProxyConnection, isAllowed func(c *mcuProxyConnection) bool) McuSubscriber { + for _, conn := range connections { + if !isAllowed(conn) || conn.IsShutdownScheduled() || conn.IsTemporary() { + continue + } + + var subscriber McuSubscriber + var err error + if conn == publisherConn { + subscriber, err = conn.newSubscriber(ctx, listener, id, publisher, streamType) + } else { + subscriber, err = conn.newRemoteSubscriber(ctx, listener, id, publisher, streamType, publisherConn) + } + if err != nil { + log.Printf("Could not create subscriber for %s publisher %s on %s: %s", streamType, publisher, conn, err) + continue + } + + return subscriber + } + + return nil +} + +func (m *mcuProxy) NewSubscriber(ctx context.Context, listener McuListener, publisher string, streamType StreamType, initiator McuInitiator) (McuSubscriber, error) { + var publisherInfo *proxyPublisherInfo + if conn := m.getPublisherConnection(publisher, streamType); conn != nil { + // Fast common path: publisher is available locally. + conn.publishersLock.Lock() + id, found := conn.publisherIds[getStreamId(publisher, streamType)] + conn.publishersLock.Unlock() + if !found { + return nil, fmt.Errorf("Unknown publisher %s", publisher) + } + + publisherInfo = &proxyPublisherInfo{ + id: id, + conn: conn, + } + } else { + log.Printf("No %s publisher %s found yet, deferring", streamType, publisher) + ch := make(chan *proxyPublisherInfo, 1) + getctx, cancel := context.WithCancel(ctx) + defer cancel() + + var wg sync.WaitGroup + + // Wait for publisher to be created locally. + wg.Add(1) + go func() { + defer wg.Done() + if conn := m.waitForPublisherConnection(getctx, publisher, streamType); conn != nil { + cancel() // Cancel pending RPC calls. + + conn.publishersLock.Lock() + id, found := conn.publisherIds[getStreamId(publisher, streamType)] + conn.publishersLock.Unlock() + if !found { + ch <- &proxyPublisherInfo{ + err: fmt.Errorf("Unknown id for local %s publisher %s", streamType, publisher), + } + return + } + + ch <- &proxyPublisherInfo{ + id: id, + conn: conn, + } + } + }() + + // Wait for publisher to be created on one of the other servers in the cluster. + if clients := m.rpcClients.GetClients(); len(clients) > 0 { + for _, client := range clients { + wg.Add(1) + go func(client *GrpcClient) { + defer wg.Done() + id, url, ip, err := client.GetPublisherId(getctx, publisher, streamType) + if errors.Is(err, context.Canceled) { + return + } else if err != nil { + log.Printf("Error getting %s publisher id %s from %s: %s", streamType, publisher, client.Target(), err) + return + } else if id == "" { + // Publisher not found on other server + return + } + + cancel() // Cancel pending RPC calls. + log.Printf("Found publisher id %s through %s on proxy %s", id, client.Target(), url) + + m.connectionsMu.RLock() + connections := m.connections + m.connectionsMu.RUnlock() + var publisherConn *mcuProxyConnection + for _, conn := range connections { + if conn.rawUrl != url || !ip.Equal(conn.ip) { + continue + } + + // Simple case, signaling server has a connection to the same endpoint + publisherConn = conn + break + } + + if publisherConn == nil { + publisherConn, err = newMcuProxyConnection(m, url, ip) + if err != nil { + log.Printf("Could not create temporary connection to %s for %s publisher %s: %s", url, streamType, publisher, err) + return + } + + publisherConn.setTemporary() + publisherConn.start() + if err := publisherConn.waitUntilConnected(ctx); err != nil { + log.Printf("Could not establish new connection to %s: %s", publisherConn, err) + publisherConn.closeIfEmpty() + return + } + + m.connectionsMu.Lock() + m.connections = append(m.connections, publisherConn) + conns, found := m.connectionsMap[url] + if found { + conns = append(conns, publisherConn) + } else { + conns = []*mcuProxyConnection{publisherConn} + } + m.connectionsMap[url] = conns + m.connectionsMu.Unlock() + } + + ch <- &proxyPublisherInfo{ + id: id, + conn: publisherConn, + } + }(client) + } + } + + wg.Wait() + select { + case ch <- &proxyPublisherInfo{ + err: fmt.Errorf("No %s publisher %s found", streamType, publisher), + }: + default: + } + + select { + case info := <-ch: + publisherInfo = info + case <-ctx.Done(): + return nil, fmt.Errorf("No %s publisher %s found", streamType, publisher) + } + } + + if publisherInfo.err != nil { + return nil, publisherInfo.err + } + + bw := publisherInfo.conn.Bandwidth() + allowOutgoing := bw == nil || bw.AllowOutgoing() + if !allowOutgoing || !publisherInfo.conn.IsSameCountry(initiator) { + connections := m.getSortedConnections(initiator) + if !allowOutgoing || len(connections) > 0 && !connections[0].IsSameCountry(publisherInfo.conn) { + // Connect to remote publisher through "closer" gateway. + subscriber := m.createSubscriber(ctx, listener, publisherInfo.id, publisher, streamType, publisherInfo.conn, connections, func(c *mcuProxyConnection) bool { + bw := c.Bandwidth() + return bw == nil || bw.AllowOutgoing() + }) + if subscriber == nil { + connections2 := make([]*mcuProxyConnection, 0, len(connections)) + for _, c := range connections { + if c.Bandwidth() != nil { + connections2 = append(connections2, c) + } + } + slices.SortFunc(connections2, func(a *mcuProxyConnection, b *mcuProxyConnection) int { + var outgoing_a *float64 + if bw := a.Bandwidth(); bw != nil { + outgoing_a = bw.Outgoing + } + + var outgoing_b *float64 + if bw := b.Bandwidth(); bw != nil { + outgoing_b = bw.Outgoing + } + + if outgoing_a == nil && outgoing_b == nil { + return 0 + } else if outgoing_a == nil && outgoing_b != nil { + return -1 + } else if outgoing_a != nil && outgoing_b == nil { + return -1 + } else if *outgoing_a < *outgoing_b { + return -1 + } else if *outgoing_a > *outgoing_b { + return 1 + } + return 0 + }) + subscriber = m.createSubscriber(ctx, listener, publisherInfo.id, publisher, streamType, publisherInfo.conn, connections2, func(c *mcuProxyConnection) bool { + return true + }) + } + if subscriber != nil { + return subscriber, nil + } + } + } + + subscriber, err := publisherInfo.conn.newSubscriber(ctx, listener, publisherInfo.id, publisher, streamType) + if err != nil { + if publisherInfo.conn.IsTemporary() { + publisherInfo.conn.closeIfEmpty() + } + log.Printf("Could not create subscriber for %s publisher %s on %s: %s", streamType, publisher, publisherInfo.conn, err) + return nil, err + } + + return subscriber, nil +} diff --git a/mcu_proxy_test.go b/mcu_proxy_test.go new file mode 100644 index 0000000..73c3260 --- /dev/null +++ b/mcu_proxy_test.go @@ -0,0 +1,1808 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2020 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "path" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/dlintw/goconf" + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.etcd.io/etcd/server/v3/embed" +) + +func TestMcuProxyStats(t *testing.T) { + collectAndLint(t, proxyMcuStats...) +} + +func newProxyConnectionWithCountry(country string) *mcuProxyConnection { + conn := &mcuProxyConnection{} + conn.country.Store(country) + return conn +} + +func Test_sortConnectionsForCountry(t *testing.T) { + conn_de := newProxyConnectionWithCountry("DE") + conn_at := newProxyConnectionWithCountry("AT") + conn_jp := newProxyConnectionWithCountry("JP") + conn_us := newProxyConnectionWithCountry("US") + + testcases := map[string][][]*mcuProxyConnection{ + // Direct country match + "DE": { + {conn_at, conn_jp, conn_de}, + {conn_de, conn_at, conn_jp}, + }, + // Direct country match + "AT": { + {conn_at, conn_jp, conn_de}, + {conn_at, conn_de, conn_jp}, + }, + // Continent match + "CH": { + {conn_de, conn_jp, conn_at}, + {conn_de, conn_at, conn_jp}, + }, + // Direct country match + "JP": { + {conn_de, conn_jp, conn_at}, + {conn_jp, conn_de, conn_at}, + }, + // Continent match + "CN": { + {conn_de, conn_jp, conn_at}, + {conn_jp, conn_de, conn_at}, + }, + // Continent match + "RU": { + {conn_us, conn_de, conn_jp, conn_at}, + {conn_de, conn_at, conn_us, conn_jp}, + }, + // No match + "AU": { + {conn_us, conn_de, conn_jp, conn_at}, + {conn_us, conn_de, conn_jp, conn_at}, + }, + } + + for country, test := range testcases { + country := country + test := test + t.Run(country, func(t *testing.T) { + sorted := sortConnectionsForCountry(test[0], country, nil) + for idx, conn := range sorted { + if test[1][idx] != conn { + assert.Fail(t, "Index %d for %s: expected %s, got %s", idx, country, test[1][idx].Country(), conn.Country()) + } + } + }) + } +} + +func Test_sortConnectionsForCountryWithOverride(t *testing.T) { + conn_de := newProxyConnectionWithCountry("DE") + conn_at := newProxyConnectionWithCountry("AT") + conn_jp := newProxyConnectionWithCountry("JP") + conn_us := newProxyConnectionWithCountry("US") + + testcases := map[string][][]*mcuProxyConnection{ + // Direct country match + "DE": { + {conn_at, conn_jp, conn_de}, + {conn_de, conn_at, conn_jp}, + }, + // Direct country match + "AT": { + {conn_at, conn_jp, conn_de}, + {conn_at, conn_de, conn_jp}, + }, + // Continent match + "CH": { + {conn_de, conn_jp, conn_at}, + {conn_de, conn_at, conn_jp}, + }, + // Direct country match + "JP": { + {conn_de, conn_jp, conn_at}, + {conn_jp, conn_de, conn_at}, + }, + // Continent match + "CN": { + {conn_de, conn_jp, conn_at}, + {conn_jp, conn_de, conn_at}, + }, + // Continent match + "RU": { + {conn_us, conn_de, conn_jp, conn_at}, + {conn_de, conn_at, conn_us, conn_jp}, + }, + // No match + "AR": { + {conn_us, conn_de, conn_jp, conn_at}, + {conn_us, conn_de, conn_jp, conn_at}, + }, + // No match but override (OC -> AS / NA) + "AU": { + {conn_us, conn_jp}, + {conn_us, conn_jp}, + }, + // No match but override (AF -> EU) + "ZA": { + {conn_de, conn_at}, + {conn_de, conn_at}, + }, + } + + continentMap := map[string][]string{ + // Use European connections for Africa. + "AF": {"EU"}, + // Use Asian and North American connections for Oceania. + "OC": {"AS", "NA"}, + } + for country, test := range testcases { + country := country + test := test + t.Run(country, func(t *testing.T) { + sorted := sortConnectionsForCountry(test[0], country, continentMap) + for idx, conn := range sorted { + if test[1][idx] != conn { + assert.Fail(t, "Index %d for %s: expected %s, got %s", idx, country, test[1][idx].Country(), conn.Country()) + } + } + }) + } +} + +type proxyServerClientHandler func(msg *ProxyClientMessage) (*ProxyServerMessage, error) + +type testProxyServerPublisher struct { + id string +} + +type testProxyServerSubscriber struct { + id string + sid string + pub *testProxyServerPublisher + + remoteUrl string +} + +type testProxyServerClient struct { + t *testing.T + + server *TestProxyServerHandler + ws *websocket.Conn + processMessage proxyServerClientHandler + + mu sync.Mutex + sessionId string +} + +func (c *testProxyServerClient) processHello(msg *ProxyClientMessage) (*ProxyServerMessage, error) { + if msg.Type != "hello" { + return nil, fmt.Errorf("expected hello, got %+v", msg) + } + + response := &ProxyServerMessage{ + Id: msg.Id, + Type: "hello", + Hello: &HelloProxyServerMessage{ + Version: "1.0", + SessionId: c.sessionId, + Server: &WelcomeServerMessage{ + Version: "1.0", + Country: c.server.country, + }, + }, + } + c.processMessage = c.processRegularMessage + return response, nil +} + +func (c *testProxyServerClient) processRegularMessage(msg *ProxyClientMessage) (*ProxyServerMessage, error) { + var handler proxyServerClientHandler + switch msg.Type { + case "command": + handler = c.processCommandMessage + } + + if handler == nil { + response := msg.NewWrappedErrorServerMessage(fmt.Errorf("type \"%s\" is not implemented", msg.Type)) + return response, nil + } + + return handler(msg) +} + +func (c *testProxyServerClient) processCommandMessage(msg *ProxyClientMessage) (*ProxyServerMessage, error) { + var response *ProxyServerMessage + switch msg.Command.Type { + case "create-publisher": + pub := c.server.createPublisher() + + if assert.NotNil(c.t, msg.Command.PublisherSettings) { + if assert.NotEqualValues(c.t, 0, msg.Command.PublisherSettings.Bitrate) { + assert.EqualValues(c.t, msg.Command.Bitrate, msg.Command.PublisherSettings.Bitrate) + } + assert.EqualValues(c.t, msg.Command.MediaTypes, msg.Command.PublisherSettings.MediaTypes) + if strings.Contains(c.t.Name(), "Codecs") { + assert.Equal(c.t, "opus,g722", msg.Command.PublisherSettings.AudioCodec) + assert.Equal(c.t, "vp9,vp8,av1", msg.Command.PublisherSettings.VideoCodec) + } else { + assert.Empty(c.t, msg.Command.PublisherSettings.AudioCodec) + assert.Empty(c.t, msg.Command.PublisherSettings.VideoCodec) + } + } + + response = &ProxyServerMessage{ + Id: msg.Id, + Type: "command", + Command: &CommandProxyServerMessage{ + Id: pub.id, + Bitrate: msg.Command.Bitrate, + }, + } + c.server.updateLoad(1) + case "delete-publisher": + if pub, found := c.server.deletePublisher(msg.Command.ClientId); !found { + response = msg.NewWrappedErrorServerMessage(fmt.Errorf("publisher %s not found", msg.Command.ClientId)) + } else { + response = &ProxyServerMessage{ + Id: msg.Id, + Type: "command", + Command: &CommandProxyServerMessage{ + Id: pub.id, + }, + } + c.server.updateLoad(-1) + } + case "create-subscriber": + var pub *testProxyServerPublisher + if msg.Command.RemoteUrl != "" { + for _, server := range c.server.servers { + if server.URL != msg.Command.RemoteUrl { + continue + } + + pub = server.getPublisher(msg.Command.PublisherId) + break + } + } else { + pub = c.server.getPublisher(msg.Command.PublisherId) + } + + if pub == nil { + response = msg.NewWrappedErrorServerMessage(fmt.Errorf("publisher %s not found", msg.Command.PublisherId)) + } else { + sub := c.server.createSubscriber(pub) + response = &ProxyServerMessage{ + Id: msg.Id, + Type: "command", + Command: &CommandProxyServerMessage{ + Id: sub.id, + Sid: sub.sid, + }, + } + c.server.updateLoad(1) + } + case "delete-subscriber": + if sub, found := c.server.deleteSubscriber(msg.Command.ClientId); !found { + response = msg.NewWrappedErrorServerMessage(fmt.Errorf("subscriber %s not found", msg.Command.ClientId)) + } else { + if msg.Command.RemoteUrl != sub.remoteUrl { + response = msg.NewWrappedErrorServerMessage(fmt.Errorf("remote subscriber %s not found", msg.Command.ClientId)) + return response, nil + } + + response = &ProxyServerMessage{ + Id: msg.Id, + Type: "command", + Command: &CommandProxyServerMessage{ + Id: sub.id, + }, + } + c.server.updateLoad(-1) + } + } + if response == nil { + response = msg.NewWrappedErrorServerMessage(fmt.Errorf("command \"%s\" is not implemented", msg.Command.Type)) + } + + return response, nil +} + +func (c *testProxyServerClient) close() { + c.mu.Lock() + defer c.mu.Unlock() + + c.ws.Close() + c.ws = nil +} + +func (c *testProxyServerClient) handleSendMessageError(fmt string, msg *ProxyServerMessage, err error) { + c.t.Helper() + + if !errors.Is(err, websocket.ErrCloseSent) || msg.Type != "event" || msg.Event.Type != "update-load" { + assert.Fail(c.t, fmt, msg, err) + } +} + +func (c *testProxyServerClient) sendMessage(msg *ProxyServerMessage) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.ws == nil { + return + } + + data, err := json.Marshal(msg) + if err != nil { + c.handleSendMessageError("error marshalling %+v: %s", msg, err) + return + } + + w, err := c.ws.NextWriter(websocket.TextMessage) + if err != nil { + c.handleSendMessageError("error creating writer for %+v: %s", msg, err) + return + } + + if _, err := w.Write(data); err != nil { + c.handleSendMessageError("error sending %+v: %s", msg, err) + return + } + + if err := w.Close(); err != nil { + c.handleSendMessageError("error during close of sending %+v: %s", msg, err) + } +} + +func (c *testProxyServerClient) run() { + defer func() { + c.mu.Lock() + defer c.mu.Unlock() + + c.server.removeClient(c) + c.ws = nil + }() + c.processMessage = c.processHello + assert := assert.New(c.t) + for { + c.mu.Lock() + ws := c.ws + c.mu.Unlock() + if ws == nil { + break + } + + msgType, reader, err := ws.NextReader() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { + assert.NoError(err) + } + return + } + + body, err := io.ReadAll(reader) + if !assert.NoError(err) { + continue + } + + if !assert.Equal(websocket.TextMessage, msgType, "unexpected message type for %s", string(body)) { + continue + } + + var msg ProxyClientMessage + if err := json.Unmarshal(body, &msg); !assert.NoError(err, "could not decode message %s", string(body)) { + continue + } + + if err := msg.CheckValid(); !assert.NoError(err, "invalid message %s", string(body)) { + continue + } + + response, err := c.processMessage(&msg) + if !assert.NoError(err) { + continue + } + + c.sendMessage(response) + if response.Type == "hello" { + c.server.sendLoad(c) + } + } +} + +type TestProxyServerHandler struct { + t *testing.T + + URL string + server *httptest.Server + servers []*TestProxyServerHandler + upgrader *websocket.Upgrader + country string + + mu sync.Mutex + load atomic.Int64 + incoming atomic.Pointer[float64] + outgoing atomic.Pointer[float64] + clients map[string]*testProxyServerClient + publishers map[string]*testProxyServerPublisher + subscribers map[string]*testProxyServerSubscriber +} + +func (h *TestProxyServerHandler) createPublisher() *testProxyServerPublisher { + h.mu.Lock() + defer h.mu.Unlock() + pub := &testProxyServerPublisher{ + id: newRandomString(32), + } + + for { + if _, found := h.publishers[pub.id]; !found { + break + } + + pub.id = newRandomString(32) + } + h.publishers[pub.id] = pub + return pub +} + +func (h *TestProxyServerHandler) getPublisher(id string) *testProxyServerPublisher { + h.mu.Lock() + defer h.mu.Unlock() + + return h.publishers[id] +} + +func (h *TestProxyServerHandler) deletePublisher(id string) (*testProxyServerPublisher, bool) { + h.mu.Lock() + defer h.mu.Unlock() + + pub, found := h.publishers[id] + if !found { + return nil, false + } + + delete(h.publishers, id) + return pub, true +} + +func (h *TestProxyServerHandler) createSubscriber(pub *testProxyServerPublisher) *testProxyServerSubscriber { + h.mu.Lock() + defer h.mu.Unlock() + + sub := &testProxyServerSubscriber{ + id: newRandomString(32), + sid: newRandomString(8), + pub: pub, + } + + for { + if _, found := h.subscribers[sub.id]; !found { + break + } + + sub.id = newRandomString(32) + } + h.subscribers[sub.id] = sub + return sub +} + +func (h *TestProxyServerHandler) deleteSubscriber(id string) (*testProxyServerSubscriber, bool) { + h.mu.Lock() + defer h.mu.Unlock() + + sub, found := h.subscribers[id] + if !found { + return nil, false + } + + delete(h.subscribers, id) + return sub, true +} + +func (h *TestProxyServerHandler) UpdateBandwidth(incoming float64, outgoing float64) { + h.incoming.Store(&incoming) + h.outgoing.Store(&outgoing) + + h.mu.Lock() + defer h.mu.Unlock() + + msg := h.getLoadMessage(h.load.Load()) + for _, c := range h.clients { + c.sendMessage(msg) + } +} + +func (h *TestProxyServerHandler) Clear(incoming bool, outgoing bool) { + if incoming { + h.incoming.Store(nil) + } + if outgoing { + h.outgoing.Store(nil) + } + + h.mu.Lock() + defer h.mu.Unlock() + + msg := h.getLoadMessage(h.load.Load()) + for _, c := range h.clients { + c.sendMessage(msg) + } +} + +func (h *TestProxyServerHandler) getLoadMessage(load int64) *ProxyServerMessage { + msg := &ProxyServerMessage{ + Type: "event", + Event: &EventProxyServerMessage{ + Type: "update-load", + Load: load, + }, + } + + incoming := h.incoming.Load() + outgoing := h.outgoing.Load() + if incoming != nil || outgoing != nil { + msg.Event.Bandwidth = &EventProxyServerBandwidth{ + Incoming: incoming, + Outgoing: outgoing, + } + } + return msg +} + +func (h *TestProxyServerHandler) updateLoad(delta int64) { + if delta == 0 { + return + } + + load := h.load.Add(delta) + + h.mu.Lock() + defer h.mu.Unlock() + + msg := h.getLoadMessage(load) + for _, c := range h.clients { + go c.sendMessage(msg) + } +} + +func (h *TestProxyServerHandler) sendLoad(c *testProxyServerClient) { + msg := h.getLoadMessage(h.load.Load()) + c.sendMessage(msg) +} + +func (h *TestProxyServerHandler) removeClient(client *testProxyServerClient) { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.clients, client.sessionId) +} + +func (h *TestProxyServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ws, err := h.upgrader.Upgrade(w, r, nil) + if !assert.NoError(h.t, err) { + return + } + + client := &testProxyServerClient{ + t: h.t, + server: h, + ws: ws, + sessionId: newRandomString(32), + } + + h.mu.Lock() + h.clients[client.sessionId] = client + h.mu.Unlock() + + go client.run() +} + +func NewProxyServerForTest(t *testing.T, country string) *TestProxyServerHandler { + t.Helper() + + upgrader := websocket.Upgrader{} + proxyHandler := &TestProxyServerHandler{ + t: t, + upgrader: &upgrader, + country: country, + clients: make(map[string]*testProxyServerClient), + publishers: make(map[string]*testProxyServerPublisher), + subscribers: make(map[string]*testProxyServerSubscriber), + } + server := httptest.NewServer(proxyHandler) + proxyHandler.server = server + proxyHandler.URL = server.URL + t.Cleanup(func() { + server.Close() + proxyHandler.mu.Lock() + defer proxyHandler.mu.Unlock() + for _, c := range proxyHandler.clients { + c.close() + } + }) + + return proxyHandler +} + +type proxyTestOptions struct { + etcd *embed.Etcd + servers []*TestProxyServerHandler +} + +func newMcuProxyForTestWithOptions(t *testing.T, options proxyTestOptions) *mcuProxy { + t.Helper() + require := require.New(t) + if options.etcd == nil { + options.etcd = NewEtcdForTest(t) + } + grpcClients, dnsMonitor := NewGrpcClientsWithEtcdForTest(t, options.etcd) + + tokenKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(err) + dir := t.TempDir() + privkeyFile := path.Join(dir, "privkey.pem") + pubkeyFile := path.Join(dir, "pubkey.pem") + WritePrivateKey(tokenKey, privkeyFile) // nolint + WritePublicKey(&tokenKey.PublicKey, pubkeyFile) // nolint + + cfg := goconf.NewConfigFile() + cfg.AddOption("mcu", "urltype", "static") + cfg.AddOption("mcu", "proxytimeout", fmt.Sprintf("%d", int(testTimeout.Seconds()))) + var urls []string + waitingMap := make(map[string]bool) + if len(options.servers) == 0 { + options.servers = []*TestProxyServerHandler{ + NewProxyServerForTest(t, "DE"), + } + } + for _, s := range options.servers { + s.servers = options.servers + urls = append(urls, s.URL) + waitingMap[s.URL] = true + } + cfg.AddOption("mcu", "url", strings.Join(urls, " ")) + cfg.AddOption("mcu", "token_id", "test-token") + cfg.AddOption("mcu", "token_key", privkeyFile) + + etcdConfig := goconf.NewConfigFile() + etcdConfig.AddOption("etcd", "endpoints", options.etcd.Config().ListenClientUrls[0].String()) + etcdConfig.AddOption("etcd", "loglevel", "error") + + etcdClient, err := NewEtcdClient(etcdConfig, "") + require.NoError(err) + t.Cleanup(func() { + assert.NoError(t, etcdClient.Close()) + }) + + mcu, err := NewMcuProxy(cfg, etcdClient, grpcClients, dnsMonitor) + require.NoError(err) + t.Cleanup(func() { + mcu.Stop() + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + require.NoError(mcu.Start(ctx)) + + proxy := mcu.(*mcuProxy) + + require.NoError(proxy.WaitForConnections(ctx)) + + for len(waitingMap) > 0 { + require.NoError(ctx.Err()) + + for u := range waitingMap { + proxy.connectionsMu.RLock() + connections := proxy.connections + proxy.connectionsMu.RUnlock() + for _, c := range connections { + if c.rawUrl == u && c.IsConnected() && c.SessionId() != "" { + delete(waitingMap, u) + break + } + } + } + + time.Sleep(time.Millisecond) + } + + return proxy +} + +func newMcuProxyForTestWithServers(t *testing.T, servers []*TestProxyServerHandler) *mcuProxy { + t.Helper() + + return newMcuProxyForTestWithOptions(t, proxyTestOptions{ + servers: servers, + }) +} + +func newMcuProxyForTest(t *testing.T) *mcuProxy { + t.Helper() + server := NewProxyServerForTest(t, "DE") + + return newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{server}) +} + +func Test_ProxyPublisherSubscriber(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + mcu := newMcuProxyForTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + + pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "DE", + } + sub, err := mcu.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + require.NoError(t, err) + + defer sub.Close(context.Background()) +} + +func Test_ProxyPublisherCodecs(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + mcu := newMcuProxyForTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + + pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + AudioCodec: "opus,g722", + VideoCodec: "vp9,vp8,av1", + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) +} + +func Test_ProxyWaitForPublisher(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + mcu := newMcuProxyForTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "DE", + } + done := make(chan struct{}) + go func() { + defer close(done) + sub, err := mcu.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + if !assert.NoError(t, err) { + return + } + + defer sub.Close(context.Background()) + }() + + // Give subscriber goroutine some time to start + time.Sleep(100 * time.Millisecond) + + pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + select { + case <-done: + case <-ctx.Done(): + assert.NoError(t, ctx.Err()) + } + defer pub.Close(context.Background()) +} + +func Test_ProxyPublisherBandwidth(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + server1 := NewProxyServerForTest(t, "DE") + server2 := NewProxyServerForTest(t, "DE") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + server1, + server2, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pub1Id := "the-publisher-1" + pub1Sid := "1234567890" + pub1Listener := &MockMcuListener{ + publicId: pub1Id + "-public", + } + pub1Initiator := &MockMcuInitiator{ + country: "DE", + } + pub1, err := mcu.NewPublisher(ctx, pub1Listener, pub1Id, pub1Sid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pub1Initiator) + require.NoError(t, err) + + defer pub1.Close(context.Background()) + + if pub1.(*mcuProxyPublisher).conn.rawUrl == server1.URL { + server1.UpdateBandwidth(100, 0) + } else { + server2.UpdateBandwidth(100, 0) + } + + // Wait until proxy has been updated + for ctx.Err() == nil { + mcu.connectionsMu.RLock() + connections := mcu.connections + mcu.connectionsMu.RUnlock() + missing := true + for _, c := range connections { + if c.Bandwidth() != nil { + missing = false + break + } + } + if !missing { + break + } + time.Sleep(time.Millisecond) + } + + pub2Id := "the-publisher-2" + pub2id := "1234567890" + pub2Listener := &MockMcuListener{ + publicId: pub2Id + "-public", + } + pub2Initiator := &MockMcuInitiator{ + country: "DE", + } + pub2, err := mcu.NewPublisher(ctx, pub2Listener, pub2Id, pub2id, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pub2Initiator) + require.NoError(t, err) + + defer pub2.Close(context.Background()) + + assert.NotEqual(t, pub1.(*mcuProxyPublisher).conn.rawUrl, pub2.(*mcuProxyPublisher).conn.rawUrl) +} + +func Test_ProxyPublisherBandwidthOverload(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + server1 := NewProxyServerForTest(t, "DE") + server2 := NewProxyServerForTest(t, "DE") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + server1, + server2, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pub1Id := "the-publisher-1" + pub1Sid := "1234567890" + pub1Listener := &MockMcuListener{ + publicId: pub1Id + "-public", + } + pub1Initiator := &MockMcuInitiator{ + country: "DE", + } + pub1, err := mcu.NewPublisher(ctx, pub1Listener, pub1Id, pub1Sid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pub1Initiator) + require.NoError(t, err) + + defer pub1.Close(context.Background()) + + // If all servers are bandwidth loaded, select the one with the least usage. + if pub1.(*mcuProxyPublisher).conn.rawUrl == server1.URL { + server1.UpdateBandwidth(100, 0) + server2.UpdateBandwidth(102, 0) + } else { + server1.UpdateBandwidth(102, 0) + server2.UpdateBandwidth(100, 0) + } + + // Wait until proxy has been updated + for ctx.Err() == nil { + mcu.connectionsMu.RLock() + connections := mcu.connections + mcu.connectionsMu.RUnlock() + missing := false + for _, c := range connections { + if c.Bandwidth() == nil { + missing = true + break + } + } + if !missing { + break + } + time.Sleep(time.Millisecond) + } + + pub2Id := "the-publisher-2" + pub2id := "1234567890" + pub2Listener := &MockMcuListener{ + publicId: pub2Id + "-public", + } + pub2Initiator := &MockMcuInitiator{ + country: "DE", + } + pub2, err := mcu.NewPublisher(ctx, pub2Listener, pub2Id, pub2id, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pub2Initiator) + require.NoError(t, err) + + defer pub2.Close(context.Background()) + + assert.Equal(t, pub1.(*mcuProxyPublisher).conn.rawUrl, pub2.(*mcuProxyPublisher).conn.rawUrl) +} + +func Test_ProxyPublisherLoad(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + server1 := NewProxyServerForTest(t, "DE") + server2 := NewProxyServerForTest(t, "DE") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + server1, + server2, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pub1Id := "the-publisher-1" + pub1Sid := "1234567890" + pub1Listener := &MockMcuListener{ + publicId: pub1Id + "-public", + } + pub1Initiator := &MockMcuInitiator{ + country: "DE", + } + pub1, err := mcu.NewPublisher(ctx, pub1Listener, pub1Id, pub1Sid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pub1Initiator) + require.NoError(t, err) + + defer pub1.Close(context.Background()) + + // Make sure connections are re-sorted. + mcu.nextSort.Store(0) + time.Sleep(100 * time.Millisecond) + + pub2Id := "the-publisher-2" + pub2id := "1234567890" + pub2Listener := &MockMcuListener{ + publicId: pub2Id + "-public", + } + pub2Initiator := &MockMcuInitiator{ + country: "DE", + } + pub2, err := mcu.NewPublisher(ctx, pub2Listener, pub2Id, pub2id, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pub2Initiator) + require.NoError(t, err) + + defer pub2.Close(context.Background()) + + assert.NotEqual(t, pub1.(*mcuProxyPublisher).conn.rawUrl, pub2.(*mcuProxyPublisher).conn.rawUrl) +} + +func Test_ProxyPublisherCountry(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + serverDE := NewProxyServerForTest(t, "DE") + serverUS := NewProxyServerForTest(t, "US") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + serverDE, + serverUS, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubDEId := "the-publisher-de" + pubDESid := "1234567890" + pubDEListener := &MockMcuListener{ + publicId: pubDEId + "-public", + } + pubDEInitiator := &MockMcuInitiator{ + country: "DE", + } + pubDE, err := mcu.NewPublisher(ctx, pubDEListener, pubDEId, pubDESid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubDEInitiator) + require.NoError(t, err) + + defer pubDE.Close(context.Background()) + + assert.Equal(t, serverDE.URL, pubDE.(*mcuProxyPublisher).conn.rawUrl) + + pubUSId := "the-publisher-us" + pubUSSid := "1234567890" + pubUSListener := &MockMcuListener{ + publicId: pubUSId + "-public", + } + pubUSInitiator := &MockMcuInitiator{ + country: "US", + } + pubUS, err := mcu.NewPublisher(ctx, pubUSListener, pubUSId, pubUSSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubUSInitiator) + require.NoError(t, err) + + defer pubUS.Close(context.Background()) + + assert.Equal(t, serverUS.URL, pubUS.(*mcuProxyPublisher).conn.rawUrl) +} + +func Test_ProxyPublisherContinent(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + serverDE := NewProxyServerForTest(t, "DE") + serverUS := NewProxyServerForTest(t, "US") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + serverDE, + serverUS, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubDEId := "the-publisher-de" + pubDESid := "1234567890" + pubDEListener := &MockMcuListener{ + publicId: pubDEId + "-public", + } + pubDEInitiator := &MockMcuInitiator{ + country: "DE", + } + pubDE, err := mcu.NewPublisher(ctx, pubDEListener, pubDEId, pubDESid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubDEInitiator) + require.NoError(t, err) + + defer pubDE.Close(context.Background()) + + assert.Equal(t, serverDE.URL, pubDE.(*mcuProxyPublisher).conn.rawUrl) + + pubFRId := "the-publisher-fr" + pubFRSid := "1234567890" + pubFRListener := &MockMcuListener{ + publicId: pubFRId + "-public", + } + pubFRInitiator := &MockMcuInitiator{ + country: "FR", + } + pubFR, err := mcu.NewPublisher(ctx, pubFRListener, pubFRId, pubFRSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubFRInitiator) + require.NoError(t, err) + + defer pubFR.Close(context.Background()) + + assert.Equal(t, serverDE.URL, pubFR.(*mcuProxyPublisher).conn.rawUrl) +} + +func Test_ProxySubscriberCountry(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + serverDE := NewProxyServerForTest(t, "DE") + serverUS := NewProxyServerForTest(t, "US") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + serverDE, + serverUS, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + assert.Equal(t, serverDE.URL, pub.(*mcuProxyPublisher).conn.rawUrl) + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "US", + } + sub, err := mcu.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + require.NoError(t, err) + + defer sub.Close(context.Background()) + + assert.Equal(t, serverUS.URL, sub.(*mcuProxySubscriber).conn.rawUrl) +} + +func Test_ProxySubscriberContinent(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + serverDE := NewProxyServerForTest(t, "DE") + serverUS := NewProxyServerForTest(t, "US") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + serverDE, + serverUS, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + assert.Equal(t, serverDE.URL, pub.(*mcuProxyPublisher).conn.rawUrl) + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "FR", + } + sub, err := mcu.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + require.NoError(t, err) + + defer sub.Close(context.Background()) + + assert.Equal(t, serverDE.URL, sub.(*mcuProxySubscriber).conn.rawUrl) +} + +func Test_ProxySubscriberBandwidth(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + serverDE := NewProxyServerForTest(t, "DE") + serverUS := NewProxyServerForTest(t, "US") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + serverDE, + serverUS, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + assert.Equal(t, serverDE.URL, pub.(*mcuProxyPublisher).conn.rawUrl) + + serverDE.UpdateBandwidth(0, 100) + + // Wait until proxy has been updated + for ctx.Err() == nil { + mcu.connectionsMu.RLock() + connections := mcu.connections + mcu.connectionsMu.RUnlock() + missing := true + for _, c := range connections { + if c.Bandwidth() != nil { + missing = false + break + } + } + if !missing { + break + } + time.Sleep(time.Millisecond) + } + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "US", + } + sub, err := mcu.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + require.NoError(t, err) + + defer sub.Close(context.Background()) + + assert.Equal(t, serverUS.URL, sub.(*mcuProxySubscriber).conn.rawUrl) +} + +func Test_ProxySubscriberBandwidthOverload(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + serverDE := NewProxyServerForTest(t, "DE") + serverUS := NewProxyServerForTest(t, "US") + mcu := newMcuProxyForTestWithServers(t, []*TestProxyServerHandler{ + serverDE, + serverUS, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + assert.Equal(t, serverDE.URL, pub.(*mcuProxyPublisher).conn.rawUrl) + + serverDE.UpdateBandwidth(0, 100) + serverUS.UpdateBandwidth(0, 102) + + // Wait until proxy has been updated + for ctx.Err() == nil { + mcu.connectionsMu.RLock() + connections := mcu.connections + mcu.connectionsMu.RUnlock() + missing := false + for _, c := range connections { + if c.Bandwidth() == nil { + missing = true + break + } + } + if !missing { + break + } + time.Sleep(time.Millisecond) + } + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "US", + } + sub, err := mcu.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + require.NoError(t, err) + + defer sub.Close(context.Background()) + + assert.Equal(t, serverDE.URL, sub.(*mcuProxySubscriber).conn.rawUrl) +} + +type mockGrpcServerHub struct { + sessionsLock sync.Mutex + sessionByPublicId map[string]Session +} + +func (h *mockGrpcServerHub) addSession(session *ClientSession) { + h.sessionsLock.Lock() + defer h.sessionsLock.Unlock() + if h.sessionByPublicId == nil { + h.sessionByPublicId = make(map[string]Session) + } + h.sessionByPublicId[session.PublicId()] = session +} + +func (h *mockGrpcServerHub) removeSession(session *ClientSession) { + h.sessionsLock.Lock() + defer h.sessionsLock.Unlock() + delete(h.sessionByPublicId, session.PublicId()) +} + +func (h *mockGrpcServerHub) GetSessionByResumeId(resumeId string) Session { + return nil +} + +func (h *mockGrpcServerHub) GetSessionByPublicId(sessionId string) Session { + h.sessionsLock.Lock() + defer h.sessionsLock.Unlock() + return h.sessionByPublicId[sessionId] +} + +func (h *mockGrpcServerHub) GetSessionIdByRoomSessionId(roomSessionId string) (string, error) { + return "", nil +} + +func (h *mockGrpcServerHub) GetBackend(u *url.URL) *Backend { + return nil +} + +func (h *mockGrpcServerHub) GetRoomForBackend(roomId string, backend *Backend) *Room { + return nil +} + +func Test_ProxyRemotePublisher(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + + etcd := NewEtcdForTest(t) + + grpcServer1, addr1 := NewGrpcServerForTest(t) + grpcServer2, addr2 := NewGrpcServerForTest(t) + + hub1 := &mockGrpcServerHub{} + hub2 := &mockGrpcServerHub{} + grpcServer1.hub = hub1 + grpcServer2.hub = hub2 + + SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) + SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) + + server1 := NewProxyServerForTest(t, "DE") + server2 := NewProxyServerForTest(t, "DE") + + mcu1 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server1, + server2, + }, + }) + mcu2 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server1, + server2, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + + session1 := &ClientSession{ + publicId: pubId, + publishers: make(map[StreamType]McuPublisher), + } + hub1.addSession(session1) + defer hub1.removeSession(session1) + + pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + session1.mu.Lock() + session1.publishers[StreamTypeVideo] = pub + session1.publisherWaiters.Wakeup() + session1.mu.Unlock() + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "DE", + } + sub, err := mcu2.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + require.NoError(t, err) + + defer sub.Close(context.Background()) +} + +func Test_ProxyMultipleRemotePublisher(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + + etcd := NewEtcdForTest(t) + + grpcServer1, addr1 := NewGrpcServerForTest(t) + grpcServer2, addr2 := NewGrpcServerForTest(t) + grpcServer3, addr3 := NewGrpcServerForTest(t) + + hub1 := &mockGrpcServerHub{} + hub2 := &mockGrpcServerHub{} + hub3 := &mockGrpcServerHub{} + grpcServer1.hub = hub1 + grpcServer2.hub = hub2 + grpcServer3.hub = hub3 + + SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) + SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) + SetEtcdValue(etcd, "/grpctargets/three", []byte("{\"address\":\""+addr3+"\"}")) + + server1 := NewProxyServerForTest(t, "DE") + server2 := NewProxyServerForTest(t, "US") + server3 := NewProxyServerForTest(t, "US") + + mcu1 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server1, + server2, + server3, + }, + }) + mcu2 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server1, + server2, + server3, + }, + }) + mcu3 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server1, + server2, + server3, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + + session1 := &ClientSession{ + publicId: pubId, + publishers: make(map[StreamType]McuPublisher), + } + hub1.addSession(session1) + defer hub1.removeSession(session1) + + pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + session1.mu.Lock() + session1.publishers[StreamTypeVideo] = pub + session1.publisherWaiters.Wakeup() + session1.mu.Unlock() + + sub1Listener := &MockMcuListener{ + publicId: "subscriber-public-1", + } + sub1Initiator := &MockMcuInitiator{ + country: "US", + } + sub1, err := mcu2.NewSubscriber(ctx, sub1Listener, pubId, StreamTypeVideo, sub1Initiator) + require.NoError(t, err) + + defer sub1.Close(context.Background()) + + sub2Listener := &MockMcuListener{ + publicId: "subscriber-public-2", + } + sub2Initiator := &MockMcuInitiator{ + country: "US", + } + sub2, err := mcu3.NewSubscriber(ctx, sub2Listener, pubId, StreamTypeVideo, sub2Initiator) + require.NoError(t, err) + + defer sub2.Close(context.Background()) +} + +func Test_ProxyRemotePublisherWait(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + + etcd := NewEtcdForTest(t) + + grpcServer1, addr1 := NewGrpcServerForTest(t) + grpcServer2, addr2 := NewGrpcServerForTest(t) + + hub1 := &mockGrpcServerHub{} + hub2 := &mockGrpcServerHub{} + grpcServer1.hub = hub1 + grpcServer2.hub = hub2 + + SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) + SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) + + server1 := NewProxyServerForTest(t, "DE") + server2 := NewProxyServerForTest(t, "DE") + + mcu1 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server1, + server2, + }, + }) + mcu2 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server1, + server2, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + + session1 := &ClientSession{ + publicId: pubId, + publishers: make(map[StreamType]McuPublisher), + } + hub1.addSession(session1) + defer hub1.removeSession(session1) + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "DE", + } + + done := make(chan struct{}) + go func() { + defer close(done) + sub, err := mcu2.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + if !assert.NoError(t, err) { + return + } + + defer sub.Close(context.Background()) + }() + + // Give subscriber goroutine some time to start + time.Sleep(100 * time.Millisecond) + + pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + session1.mu.Lock() + session1.publishers[StreamTypeVideo] = pub + session1.publisherWaiters.Wakeup() + session1.mu.Unlock() + + select { + case <-done: + case <-ctx.Done(): + assert.NoError(t, ctx.Err()) + } +} + +func Test_ProxyRemotePublisherTemporary(t *testing.T) { + CatchLogForTest(t) + t.Parallel() + + etcd := NewEtcdForTest(t) + + grpcServer1, addr1 := NewGrpcServerForTest(t) + grpcServer2, addr2 := NewGrpcServerForTest(t) + + hub1 := &mockGrpcServerHub{} + hub2 := &mockGrpcServerHub{} + grpcServer1.hub = hub1 + grpcServer2.hub = hub2 + + SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) + SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) + + server1 := NewProxyServerForTest(t, "DE") + server2 := NewProxyServerForTest(t, "DE") + + mcu1 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server1, + }, + }) + mcu2 := newMcuProxyForTestWithOptions(t, proxyTestOptions{ + etcd: etcd, + servers: []*TestProxyServerHandler{ + server2, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + pubId := "the-publisher" + pubSid := "1234567890" + pubListener := &MockMcuListener{ + publicId: pubId + "-public", + } + pubInitiator := &MockMcuInitiator{ + country: "DE", + } + + session1 := &ClientSession{ + publicId: pubId, + publishers: make(map[StreamType]McuPublisher), + } + hub1.addSession(session1) + defer hub1.removeSession(session1) + + pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, StreamTypeVideo, NewPublisherSettings{ + MediaTypes: MediaTypeVideo | MediaTypeAudio, + }, pubInitiator) + require.NoError(t, err) + + defer pub.Close(context.Background()) + + session1.mu.Lock() + session1.publishers[StreamTypeVideo] = pub + session1.publisherWaiters.Wakeup() + session1.mu.Unlock() + + mcu2.connectionsMu.RLock() + count := len(mcu2.connections) + mcu2.connectionsMu.RUnlock() + assert.Equal(t, 1, count) + + subListener := &MockMcuListener{ + publicId: "subscriber-public", + } + subInitiator := &MockMcuInitiator{ + country: "DE", + } + sub, err := mcu2.NewSubscriber(ctx, subListener, pubId, StreamTypeVideo, subInitiator) + require.NoError(t, err) + + defer sub.Close(context.Background()) + + assert.Equal(t, server1.URL, sub.(*mcuProxySubscriber).conn.rawUrl) + + // The temporary connection has been added + mcu2.connectionsMu.RLock() + count = len(mcu2.connections) + mcu2.connectionsMu.RUnlock() + assert.Equal(t, 2, count) + + sub.Close(context.Background()) + + // Wait for temporary connection to be removed. +loop: + for { + select { + case <-ctx.Done(): + assert.NoError(t, ctx.Err()) + default: + mcu2.connectionsMu.RLock() + count = len(mcu2.connections) + mcu2.connectionsMu.RUnlock() + if count == 1 { + break loop + } + } + } +} diff --git a/mcu_stats_prometheus.go b/mcu_stats_prometheus.go new file mode 100644 index 0000000..0d0e9ca --- /dev/null +++ b/mcu_stats_prometheus.go @@ -0,0 +1,131 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2021 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +var ( + statsPublishersCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "publishers", + Help: "The current number of publishers", + }, []string{"type"}) + statsPublishersTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "publishers_total", + Help: "The total number of created publishers", + }, []string{"type"}) + statsSubscribersCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "subscribers", + Help: "The current number of subscribers", + }, []string{"type"}) + statsSubscribersTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "subscribers_total", + Help: "The total number of created subscribers", + }, []string{"type"}) + statsWaitingForPublisherTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "nopublisher_total", + Help: "The total number of subscribe requests where no publisher exists", + }, []string{"type"}) + statsMcuMessagesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "messages_total", + Help: "The total number of MCU messages", + }, []string{"type"}) + statsMcuSubscriberStreamTypesCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "subscriber_streams", + Help: "The current number of subscribed media streams", + }, []string{"type"}) + statsMcuPublisherStreamTypesCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "publisher_streams", + Help: "The current number of published media streams", + }, []string{"type"}) + + commonMcuStats = []prometheus.Collector{ + statsPublishersCurrent, + statsPublishersTotal, + statsSubscribersCurrent, + statsSubscribersTotal, + statsWaitingForPublisherTotal, + statsMcuMessagesTotal, + statsMcuSubscriberStreamTypesCurrent, + statsMcuPublisherStreamTypesCurrent, + } + + statsConnectedProxyBackendsCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "backend_connections", + Help: "Current number of connections to signaling proxy backends", + }, []string{"country"}) + statsProxyBackendLoadCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "backend_load", + Help: "Current load of signaling proxy backends", + }, []string{"url"}) + statsProxyNobackendAvailableTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "signaling", + Subsystem: "mcu", + Name: "no_backend_available_total", + Help: "Total number of publishing requests where no backend was available", + }, []string{"type"}) + + proxyMcuStats = []prometheus.Collector{ + statsConnectedProxyBackendsCurrent, + statsProxyBackendLoadCurrent, + statsProxyNobackendAvailableTotal, + } +) + +func RegisterJanusMcuStats() { + registerAll(commonMcuStats...) +} + +func UnregisterJanusMcuStats() { + unregisterAll(commonMcuStats...) +} + +func RegisterProxyMcuStats() { + registerAll(commonMcuStats...) + registerAll(proxyMcuStats...) +} + +func UnregisterProxyMcuStats() { + unregisterAll(commonMcuStats...) + unregisterAll(proxyMcuStats...) +} diff --git a/mcu_test.go b/mcu_test.go new file mode 100644 index 0000000..1fb6841 --- /dev/null +++ b/mcu_test.go @@ -0,0 +1,273 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2019 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "errors" + "fmt" + "log" + "sync" + "sync/atomic" + + "github.com/dlintw/goconf" +) + +const ( + TestMaxBitrateScreen = 12345678 + TestMaxBitrateVideo = 23456789 +) + +type TestMCU struct { + mu sync.Mutex + publishers map[string]*TestMCUPublisher + subscribers map[string]*TestMCUSubscriber +} + +func NewTestMCU() (*TestMCU, error) { + return &TestMCU{ + publishers: make(map[string]*TestMCUPublisher), + subscribers: make(map[string]*TestMCUSubscriber), + }, nil +} + +func (m *TestMCU) Start(ctx context.Context) error { + return nil +} + +func (m *TestMCU) Stop() { +} + +func (m *TestMCU) Reload(config *goconf.ConfigFile) { +} + +func (m *TestMCU) SetOnConnected(f func()) { +} + +func (m *TestMCU) SetOnDisconnected(f func()) { +} + +func (m *TestMCU) GetStats() interface{} { + return nil +} + +func (m *TestMCU) NewPublisher(ctx context.Context, listener McuListener, id string, sid string, streamType StreamType, settings NewPublisherSettings, initiator McuInitiator) (McuPublisher, error) { + var maxBitrate int + if streamType == StreamTypeScreen { + maxBitrate = TestMaxBitrateScreen + } else { + maxBitrate = TestMaxBitrateVideo + } + publisherSettings := settings + bitrate := publisherSettings.Bitrate + if bitrate <= 0 || bitrate > maxBitrate { + publisherSettings.Bitrate = maxBitrate + } + pub := &TestMCUPublisher{ + TestMCUClient: TestMCUClient{ + id: id, + sid: sid, + streamType: streamType, + }, + + settings: publisherSettings, + } + + m.mu.Lock() + defer m.mu.Unlock() + + m.publishers[id] = pub + return pub, nil +} + +func (m *TestMCU) GetPublishers() map[string]*TestMCUPublisher { + m.mu.Lock() + defer m.mu.Unlock() + + result := make(map[string]*TestMCUPublisher, len(m.publishers)) + for id, pub := range m.publishers { + result[id] = pub + } + return result +} + +func (m *TestMCU) GetPublisher(id string) *TestMCUPublisher { + m.mu.Lock() + defer m.mu.Unlock() + + return m.publishers[id] +} + +func (m *TestMCU) NewSubscriber(ctx context.Context, listener McuListener, publisher string, streamType StreamType, initiator McuInitiator) (McuSubscriber, error) { + m.mu.Lock() + defer m.mu.Unlock() + + pub := m.publishers[publisher] + if pub == nil { + return nil, fmt.Errorf("Waiting for publisher not implemented yet") + } + + id := newRandomString(8) + sub := &TestMCUSubscriber{ + TestMCUClient: TestMCUClient{ + id: id, + streamType: streamType, + }, + + publisher: pub, + } + return sub, nil +} + +type TestMCUClient struct { + closed atomic.Bool + + id string + sid string + streamType StreamType +} + +func (c *TestMCUClient) Id() string { + return c.id +} + +func (c *TestMCUClient) Sid() string { + return c.sid +} + +func (c *TestMCUClient) StreamType() StreamType { + return c.streamType +} + +func (c *TestMCUClient) MaxBitrate() int { + return 0 +} + +func (c *TestMCUClient) Close(ctx context.Context) { + if c.closed.CompareAndSwap(false, true) { + log.Printf("Close MCU client %s", c.id) + } +} + +func (c *TestMCUClient) isClosed() bool { + return c.closed.Load() +} + +type TestMCUPublisher struct { + TestMCUClient + + settings NewPublisherSettings + + sdp string +} + +func (p *TestMCUPublisher) HasMedia(mt MediaType) bool { + return (p.settings.MediaTypes & mt) == mt +} + +func (p *TestMCUPublisher) SetMedia(mt MediaType) { + p.settings.MediaTypes = mt +} + +func (p *TestMCUPublisher) SendMessage(ctx context.Context, message *MessageClientMessage, data *MessageClientMessageData, callback func(error, map[string]interface{})) { + go func() { + if p.isClosed() { + callback(fmt.Errorf("Already closed"), nil) + return + } + + switch data.Type { + case "offer": + sdp := data.Payload["sdp"] + if sdp, ok := sdp.(string); ok { + p.sdp = sdp + if sdp == MockSdpOfferAudioOnly { + callback(nil, map[string]interface{}{ + "type": "answer", + "sdp": MockSdpAnswerAudioOnly, + }) + return + } else if sdp == MockSdpOfferAudioAndVideo { + callback(nil, map[string]interface{}{ + "type": "answer", + "sdp": MockSdpAnswerAudioAndVideo, + }) + return + } + } + callback(fmt.Errorf("Offer payload %+v is not implemented", data.Payload), nil) + default: + callback(fmt.Errorf("Message type %s is not implemented", data.Type), nil) + } + }() +} + +func (p *TestMCUPublisher) GetStreams(ctx context.Context) ([]PublisherStream, error) { + return nil, errors.New("not implemented") +} + +func (p *TestMCUPublisher) PublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { + return errors.New("remote publishing not supported") +} + +func (p *TestMCUPublisher) UnpublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { + return errors.New("remote publishing not supported") +} + +type TestMCUSubscriber struct { + TestMCUClient + + publisher *TestMCUPublisher +} + +func (s *TestMCUSubscriber) Publisher() string { + return s.publisher.id +} + +func (s *TestMCUSubscriber) SendMessage(ctx context.Context, message *MessageClientMessage, data *MessageClientMessageData, callback func(error, map[string]interface{})) { + go func() { + if s.isClosed() { + callback(fmt.Errorf("Already closed"), nil) + return + } + + switch data.Type { + case "requestoffer": + fallthrough + case "sendoffer": + sdp := s.publisher.sdp + if sdp == "" { + callback(fmt.Errorf("Publisher not sending (no SDP)"), nil) + return + } + + callback(nil, map[string]interface{}{ + "type": "offer", + "sdp": sdp, + }) + case "answer": + callback(nil, nil) + default: + callback(fmt.Errorf("Message type %s is not implemented", data.Type), nil) + } + }() +} diff --git a/metrics/prometheus.go b/metrics/prometheus.go deleted file mode 100644 index 93ad0ba..0000000 --- a/metrics/prometheus.go +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package metrics - -import ( - "github.com/prometheus/client_golang/prometheus" -) - -func RegisterAll(cs ...prometheus.Collector) { - for _, c := range cs { - if err := prometheus.DefaultRegisterer.Register(c); err != nil { - if _, ok := err.(prometheus.AlreadyRegisteredError); !ok { - panic(err) - } - } - } -} - -func UnregisterAll(cs ...prometheus.Collector) { - for _, c := range cs { - prometheus.Unregister(c) - } -} diff --git a/metrics/prometheus_test.go b/metrics/prometheus_test.go deleted file mode 100644 index 801ae3f..0000000 --- a/metrics/prometheus_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package metrics - -import ( - "testing" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" -) - -func TestRegistration(t *testing.T) { - t.Parallel() - - collectors := []prometheus.Collector{ - prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "test", - Name: "value_total", - Help: "Total value.", - }), - prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "test", - Name: "value", - Help: "Current value.", - }, []string{"foo", "bar"}), - } - // Can unregister without previous registration - UnregisterAll(collectors...) - RegisterAll(collectors...) - // Can register multiple times - RegisterAll(collectors...) - UnregisterAll(collectors...) -} - -func TestRegistrationError(t *testing.T) { - t.Parallel() - - defer func() { - value := recover() - if err, ok := value.(error); assert.True(t, ok) { - assert.ErrorContains(t, err, "is not a valid metric name") - } - }() - - RegisterAll(prometheus.NewCounter(prometheus.CounterOpts{})) -} diff --git a/metrics/test/metrics_test.go b/metrics/test/metrics_test.go deleted file mode 100644 index d6c4a65..0000000 --- a/metrics/test/metrics_test.go +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "testing" - - "github.com/prometheus/client_golang/prometheus" -) - -func TestMetrics(t *testing.T) { // nolint:paralleltest - gauge := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "metrics_tests", - Name: "value", - Help: "Current value for the metrics tests", - }) - - CollectAndLint(t, gauge) - - CheckStatsValue(t, gauge, 0) - - gauge.Inc() - CheckStatsValue(t, gauge, 1) - - ResetStatsValue(t, gauge) - CheckStatsValue(t, gauge, 0) -} - -func TestAssertCollectorChangeBy(t *testing.T) { - t.Parallel() - - gauge := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "metrics_tests", - Name: "value", - Help: "Current value for the metrics tests", - }) - - AssertCollectorChangeBy(t, gauge, 1) - gauge.Inc() -} diff --git a/mock/data.go b/mock_data_test.go similarity index 77% rename from mock/data.go rename to mock_data_test.go index fa4def2..f90927a 100644 --- a/mock/data.go +++ b/mock_data_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package mock +package signaling const ( // See https://tools.ietf.org/id/draft-ietf-rtcweb-sdp-08.html#rfc.section.5.2.1 @@ -168,62 +168,5 @@ a=rtcp-fb:99 nack a=rtcp-fb:99 nack pli a=rtcp-fb:99 ccm fir a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:mid -` - - MockSdpOfferAudioOnlyNoFilter = `v=0 -o=- 20518 0 IN IP4 0.0.0.0 -s=- -t=0 0 -a=group:BUNDLE audio-D.ietf-mmusic-sdp-bundle-negotiation -a=ice-options:trickle-D.ietf-mmusic-trickle-ice -m=audio 54609 UDP/TLS/RTP/SAVPF 109 0 8 -c=IN IP4 192.168.0.1 -a=mid:audio -a=msid:ma ta -a=sendrecv -a=rtpmap:109 opus/48000/2 -a=rtpmap:0 PCMU/8000 -a=rtpmap:8 PCMA/8000 -a=maxptime:120 -a=ice-ufrag:074c6550 -a=ice-pwd:a28a397a4c3f31747d1ee3474af08a068 -a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2 -a=setup:actpass -a=tls-id:1 -a=rtcp-mux -a=rtcp:60065 IN IP4 192.168.0.1 -a=rtcp-rsize -a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level -a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:mid -a=candidate:0 1 UDP 2122194687 192.0.2.4 61665 typ host -a=candidate:0 2 UDP 2122194687 192.0.2.4 61667 typ host -a=end-of-candidates -` - MockSdpAnswerAudioOnlyNoFilter = `v=0 -o=- 16833 0 IN IP4 0.0.0.0 -s=- -t=0 0 -a=group:BUNDLE audio -a=ice-options:trickle -m=audio 49203 UDP/TLS/RTP/SAVPF 109 0 8 -c=IN IP4 192.168.0.1 -a=mid:audio -a=msid:ma ta -a=sendrecv -a=rtpmap:109 opus/48000/2 -a=rtpmap:0 PCMU/8000 -a=rtpmap:8 PCMA/8000 -a=maxptime:120 -a=ice-ufrag:05067423 -a=ice-pwd:1747d1ee3474a28a397a4c3f3af08a068 -a=fingerprint:sha-256 6B:8B:F0:65:5F:78:E2:51:3B:AC:6F:F3:3F:46:1B:35:DC:B8:5F:64:1A:24:C2:43:F0:A1:58:D0:A1:2C:19:08 -a=setup:active -a=tls-id:1 -a=rtcp-mux -a=rtcp-rsize -a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level -a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:mid -a=candidate:0 1 UDP 2122194687 198.51.100.7 51556 typ host -a=end-of-candidates ` ) diff --git a/nats/client_test.go b/nats/client_test.go deleted file mode 100644 index 06894c5..0000000 --- a/nats/client_test.go +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package nats - -import ( - "testing" - - "github.com/nats-io/nats.go" - "github.com/stretchr/testify/assert" -) - -func TestGetEncodedSubject(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - encoded := GetEncodedSubject("foo", "this is the subject") - assert.NotContains(encoded, " ") - - encoded = GetEncodedSubject("foo", "this-is-the-subject") - assert.NotContains(encoded, "this-is") -} - -func TestDecodeToString(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - testcases := []struct { - data []byte - expected string - }{ - { - []byte(`""`), - "", - }, - { - []byte(`"foo"`), - "foo", - }, - { - []byte(`{"type":"foo"}`), - `{"type":"foo"}`, - }, - { - []byte(`1234`), - "1234", - }, - } - - for idx, tc := range testcases { - var dest string - if assert.NoError(Decode(&nats.Msg{ - Data: tc.data, - }, &dest), "decoding failed for test %d (%s)", idx, string(tc.data)) { - assert.Equal(tc.expected, dest, "failed for test %s (%s)", idx, string(tc.data)) - } - } -} - -func TestDecodeToByteSlice(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - testcases := []struct { - data []byte - expected []byte - }{ - { - []byte(``), - []byte{}, - }, - { - []byte(`""`), - []byte(`""`), - }, - { - []byte(`"foo"`), - []byte(`"foo"`), - }, - { - []byte(`{"type":"foo"}`), - []byte(`{"type":"foo"}`), - }, - { - []byte(`1234`), - []byte(`1234`), - }, - } - - for idx, tc := range testcases { - var dest []byte - if assert.NoError(Decode(&nats.Msg{ - Data: tc.data, - }, &dest), "decoding failed for test %d (%s)", idx, string(tc.data)) { - assert.Equal(tc.expected, dest, "failed for test %s (%s)", idx, string(tc.data)) - } - } -} - -func TestDecodeRegular(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - type testdata struct { - Type string `json:"type"` - Value any `json:"value"` - } - - testcases := []struct { - data []byte - expected *testdata - }{ - { - []byte(`null`), - nil, - }, - { - []byte(`{"value":"bar","type":"foo"}`), - &testdata{ - Type: "foo", - Value: "bar", - }, - }, - { - []byte(`{"value":123,"type":"foo"}`), - &testdata{ - Type: "foo", - Value: float64(123), - }, - }, - } - - for idx, tc := range testcases { - var dest *testdata - if assert.NoError(Decode(&nats.Msg{ - Data: tc.data, - }, &dest), "decoding failed for test %d (%s)", idx, string(tc.data)) { - assert.Equal(tc.expected, dest, "failed for test %s (%s)", idx, string(tc.data)) - } - } -} diff --git a/nats/native.go b/nats/native.go deleted file mode 100644 index 49b78f0..0000000 --- a/nats/native.go +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package nats - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/nats-io/nats.go" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" -) - -type NativeClient struct { - logger log.Logger - conn *nats.Conn - closed chan struct{} -} - -func (c *NativeClient) URLs() []string { - return c.conn.Servers() -} - -func (c *NativeClient) IsConnected() bool { - return c.conn.IsConnected() -} - -func (c *NativeClient) ConnectedUrl() string { - return c.conn.ConnectedUrl() -} - -func (c *NativeClient) ConnectedServerId() string { - return c.conn.ConnectedServerId() -} - -func (c *NativeClient) ConnectedServerVersion() string { - return c.conn.ConnectedServerVersion() -} - -func (c *NativeClient) ConnectedClusterName() string { - return c.conn.ConnectedClusterName() -} - -func (c *NativeClient) Close(ctx context.Context) error { - c.conn.Close() - select { - case <-c.closed: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -func (c *NativeClient) FlushWithContext(ctx context.Context) error { - return c.conn.FlushWithContext(ctx) -} - -func (c *NativeClient) onClosed(conn *nats.Conn) { - if err := conn.LastError(); err != nil { - c.logger.Printf("NATS client closed, last error %s", conn.LastError()) - } else { - c.logger.Println("NATS client closed") - } - close(c.closed) -} - -func (c *NativeClient) onDisconnected(conn *nats.Conn) { - c.logger.Println("NATS client disconnected") -} - -func (c *NativeClient) onReconnected(conn *nats.Conn) { - c.logger.Printf("NATS client reconnected to %s (%s)", conn.ConnectedUrl(), conn.ConnectedServerId()) -} - -func (c *NativeClient) Subscribe(subject string, ch chan *Msg) (Subscription, error) { - return c.conn.ChanSubscribe(subject, ch) -} - -func (c *NativeClient) Publish(subject string, message any) error { - data, err := json.Marshal(message) - if err != nil { - return err - } - - return c.conn.Publish(subject, data) -} - -func removeURLCredentials(u string) string { - if u, err := url.Parse(u); err == nil && u.User != nil { - u.User = url.User("***") - return u.String() - } - return u -} diff --git a/nats/native_test.go b/nats/native_test.go deleted file mode 100644 index 6a1eeeb..0000000 --- a/nats/native_test.go +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package nats - -import ( - "context" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/nats-io/nats-server/v2/server" - natsservertest "github.com/nats-io/nats-server/v2/test" - "github.com/nats-io/nats.go" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" -) - -func StartLocalServer(t *testing.T) (*server.Server, int) { - t.Helper() - return StartLocalServerPort(t, server.RANDOM_PORT) -} - -func StartLocalServerPort(t *testing.T, port int) (*server.Server, int) { - t.Helper() - opts := natsservertest.DefaultTestOptions - opts.Port = port - opts.Cluster.Name = "testing" - srv := natsservertest.RunServer(&opts) - t.Cleanup(func() { - srv.Shutdown() - srv.WaitForShutdown() - }) - return srv, opts.Port -} - -func CreateLocalClientForTest(t *testing.T, options ...nats.Option) (*server.Server, int, Client) { - t.Helper() - server, port := StartLocalServer(t) - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - result, err := NewClient(ctx, server.ClientURL(), options...) - require.NoError(t, err) - t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - assert.NoError(t, result.Close(ctx)) - }) - return server, port, result -} - -func testClient_Subscribe(t *testing.T, client Client) { - require := require.New(t) - assert := assert.New(t) - dest := make(chan *Msg) - sub, err := client.Subscribe("foo", dest) - require.NoError(err) - ch := make(chan struct{}) - - var received atomic.Int32 - maxPublish := int32(20) - ready := make(chan struct{}) - quit := make(chan struct{}) - defer close(quit) - go func() { - close(ready) - for { - select { - case <-dest: - total := received.Add(1) - if total == maxPublish { - if err := sub.Unsubscribe(); !assert.NoError(err) { - return - } - close(ch) - } - case <-quit: - return - } - } - }() - <-ready - for range maxPublish { - assert.NoError(client.Publish("foo", []byte("hello"))) - - // Allow NATS goroutines to process messages. - time.Sleep(10 * time.Millisecond) - } - <-ch - - require.Equal(maxPublish, received.Load(), "Received wrong # of messages") -} - -func TestClient_Subscribe(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - _, _, client := CreateLocalClientForTest(t) - - testClient_Subscribe(t, client) - }) -} - -func test_PublishAfterClose(t *testing.T, client Client) { - assert.NoError(t, client.Close(t.Context())) - - assert.ErrorIs(t, client.Publish("foo", "bar"), nats.ErrConnectionClosed) -} - -func TestClient_PublishAfterClose(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - _, _, client := CreateLocalClientForTest(t) - - test_PublishAfterClose(t, client) - }) -} - -func testClient_SubscribeAfterClose(t *testing.T, client Client) { - assert.NoError(t, client.Close(t.Context())) - - ch := make(chan *Msg) - _, err := client.Subscribe("foo", ch) - assert.ErrorIs(t, err, nats.ErrConnectionClosed) -} - -func TestClient_SubscribeAfterClose(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - _, _, client := CreateLocalClientForTest(t) - - testClient_SubscribeAfterClose(t, client) - }) -} - -func testClient_BadSubjects(t *testing.T, client Client) { - assert := assert.New(t) - subjects := []string{ - "foo bar", - "foo.", - } - - ch := make(chan *Msg) - for _, s := range subjects { - _, err := client.Subscribe(s, ch) - assert.ErrorIs(err, nats.ErrBadSubject, "Expected error for subject %s", s) - } -} - -func TestClient_BadSubjects(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - _, _, client := CreateLocalClientForTest(t) - - testClient_BadSubjects(t, client) - }) -} - -func TestClient_MaxReconnects(t *testing.T) { // nolint:paralleltest - test.EnsureNoGoroutinesLeak(t, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - reconnectWait := time.Millisecond - server, port, client := CreateLocalClientForTest(t, - nats.ReconnectWait(reconnectWait), - nats.ReconnectJitter(0, 0), - ) - c, ok := client.(*NativeClient) - require.True(ok, "wrong class: %T", client) - require.True(c.conn.IsConnected(), "not connected initially") - assert.Equal(server.ID(), c.conn.ConnectedServerId()) - - server.Shutdown() - server.WaitForShutdown() - - // The NATS client tries to reconnect a maximum of 100 times by default. - time.Sleep(100 * reconnectWait) - for i := 0; i < 1000 && c.conn.IsConnected(); i++ { - time.Sleep(time.Millisecond) - } - require.False(c.conn.IsConnected(), "should be disconnected after server shutdown") - - server, _ = StartLocalServerPort(t, port) - - // Wait for automatic reconnection - for i := 0; i < 1000 && !c.conn.IsConnected(); i++ { - time.Sleep(time.Millisecond) - } - require.True(c.conn.IsConnected(), "not connected after restart") - assert.Equal(server.ID(), c.conn.ConnectedServerId()) - }) -} diff --git a/nats/test/server.go b/nats/test/server.go deleted file mode 100644 index 6daff42..0000000 --- a/nats/test/server.go +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "testing" - "time" - - "github.com/nats-io/nats-server/v2/server" - "github.com/nats-io/nats-server/v2/test" - "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" -) - -func StartLocalServer(t *testing.T) (*server.Server, int) { - t.Helper() - return StartLocalServerPort(t, server.RANDOM_PORT) -} - -func StartLocalServerPort(t *testing.T, port int) (*server.Server, int) { - t.Helper() - opts := test.DefaultTestOptions - opts.Port = port - opts.Cluster.Name = "testing" - srv := test.RunServer(&opts) - t.Cleanup(func() { - srv.Shutdown() - srv.WaitForShutdown() - }) - return srv, opts.Port -} - -func WaitForSubscriptionsEmpty(ctx context.Context, t *testing.T, client nats.Client) { - t.Helper() - if c, ok := client.(*nats.LoopbackClient); assert.True(t, ok, "expected LoopbackNatsClient, got %T", client) { - for { - remaining := c.SubscriptionCount() - if remaining == 0 { - break - } - - select { - case <-ctx.Done(): - assert.NoError(t, ctx.Err(), "Error waiting for %d subscriptions to terminate", remaining) - return - default: - time.Sleep(time.Millisecond) - } - } - } -} diff --git a/nats/test/server_test.go b/nats/test/server_test.go deleted file mode 100644 index 190d2b7..0000000 --- a/nats/test/server_test.go +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" -) - -func TestLocalServer(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - server, port := StartLocalServer(t) - assert.NotEqual(0, port) - - ctx := log.NewLoggerContext(t.Context(), logtest.NewLoggerForTest(t)) - - client, err := nats.NewClient(ctx, server.ClientURL()) - require.NoError(err) - - assert.NoError(client.Close(context.Background())) -} - -func TestWaitForSubscriptionsEmpty(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - ctx := log.NewLoggerContext(t.Context(), logtest.NewLoggerForTest(t)) - - client, err := nats.NewClient(ctx, nats.LoopbackUrl) - require.NoError(err) - defer func() { - assert.NoError(client.Close(context.Background())) - }() - - ch := make(chan *nats.Msg) - sub, err := client.Subscribe("foo", ch) - require.NoError(err) - - ready := make(chan struct{}) - done := make(chan struct{}) - go func() { - defer close(done) - - ctx, cancel := context.WithTimeout(t.Context(), time.Second) - defer cancel() - - close(ready) - WaitForSubscriptionsEmpty(ctx, t, client) - }() - - <-ready - - require.NoError(sub.Unsubscribe()) - <-done -} diff --git a/nats/client.go b/natsclient.go similarity index 54% rename from nats/client.go rename to natsclient.go index 027d691..3c2f6d1 100644 --- a/nats/client.go +++ b/natsclient.go @@ -1,6 +1,6 @@ /** * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG + * Copyright (C) 2017 struktur AG * * @author Joachim Bauch * @@ -19,47 +19,40 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package nats +package signaling import ( "context" "encoding/base64" "encoding/json" - "errors" + "fmt" + "log" "os" "os/signal" "strings" "time" "github.com/nats-io/nats.go" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) const ( initialConnectInterval = time.Second maxConnectInterval = 8 * time.Second - LoopbackUrl = "nats://loopback" - - DefaultURL = nats.DefaultURL + NatsLoopbackUrl = "nats://loopback" ) -var ( - ErrConnectionClosed = nats.ErrConnectionClosed -) - -type Msg = nats.Msg - -type Subscription interface { +type NatsSubscription interface { Unsubscribe() error } -type Client interface { - Close(ctx context.Context) error +type NatsClient interface { + Close() - Subscribe(subject string, ch chan *Msg) (Subscription, error) - Publish(subject string, message any) error + Subscribe(subject string, ch chan *nats.Msg) (NatsSubscription, error) + Publish(subject string, message interface{}) error + + Decode(msg *nats.Msg, v interface{}) error } // The NATS client doesn't work if a subject contains spaces. As the room id @@ -69,53 +62,79 @@ func GetEncodedSubject(prefix string, suffix string) string { return prefix + "." + base64.StdEncoding.EncodeToString([]byte(suffix)) } -func NewClient(ctx context.Context, url string, options ...nats.Option) (Client, error) { - logger := log.LoggerFromContext(ctx) +type natsClient struct { + conn *nats.Conn +} + +func NewNatsClient(url string) (NatsClient, error) { if url == ":loopback:" { - logger.Printf("WARNING: events url %s is deprecated, please use %s instead", url, LoopbackUrl) - url = LoopbackUrl + log.Printf("WARNING: events url %s is deprecated, please use %s instead", url, NatsLoopbackUrl) + url = NatsLoopbackUrl } - if url == LoopbackUrl { - logger.Println("Using internal NATS loopback client") - return NewLoopbackClient(logger) + if url == NatsLoopbackUrl { + log.Println("Using internal NATS loopback client") + return NewLoopbackNatsClient() } - backoff, err := async.NewExponentialBackoff(initialConnectInterval, maxConnectInterval) + backoff, err := NewExponentialBackoff(initialConnectInterval, maxConnectInterval) if err != nil { return nil, err } - client := &NativeClient{ - logger: logger, - closed: make(chan struct{}), - } + client := &natsClient{} - options = append([]nats.Option{ + client.conn, err = nats.Connect(url, nats.ClosedHandler(client.onClosed), nats.DisconnectHandler(client.onDisconnected), - nats.ReconnectHandler(client.onReconnected), - nats.MaxReconnects(-1), - }, options...) - client.conn, err = nats.Connect(url, options...) + nats.ReconnectHandler(client.onReconnected)) - ctx, stop := signal.NotifyContext(ctx, os.Interrupt) + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() // The initial connect must succeed, so we retry in the case of an error. for err != nil { - logger.Printf("Could not create connection (%s), will retry in %s", err, backoff.NextWait()) + log.Printf("Could not create connection (%s), will retry in %s", err, backoff.NextWait()) backoff.Wait(ctx) if ctx.Err() != nil { - return nil, errors.New("interrupted") + return nil, fmt.Errorf("interrupted") } client.conn, err = nats.Connect(url) } - logger.Printf("Connection established to %s (%s)", removeURLCredentials(client.conn.ConnectedUrl()), client.conn.ConnectedServerId()) + log.Printf("Connection established to %s (%s)", client.conn.ConnectedUrl(), client.conn.ConnectedServerId()) return client, nil } -func Decode(msg *nats.Msg, vPtr any) (err error) { +func (c *natsClient) Close() { + c.conn.Close() +} + +func (c *natsClient) onClosed(conn *nats.Conn) { + log.Println("NATS client closed", conn.LastError()) +} + +func (c *natsClient) onDisconnected(conn *nats.Conn) { + log.Println("NATS client disconnected") +} + +func (c *natsClient) onReconnected(conn *nats.Conn) { + log.Printf("NATS client reconnected to %s (%s)", conn.ConnectedUrl(), conn.ConnectedServerId()) +} + +func (c *natsClient) Subscribe(subject string, ch chan *nats.Msg) (NatsSubscription, error) { + return c.conn.ChanSubscribe(subject, ch) +} + +func (c *natsClient) Publish(subject string, message interface{}) error { + data, err := json.Marshal(message) + if err != nil { + return err + } + + return c.conn.Publish(subject, data) +} + +func (c *natsClient) Decode(msg *nats.Msg, vPtr interface{}) (err error) { switch arg := vPtr.(type) { case *string: // If they want a string and it is a JSON string, strip quotes diff --git a/nats/loopback.go b/natsclient_loopback.go similarity index 62% rename from nats/loopback.go rename to natsclient_loopback.go index a0c83ef..56b6fb6 100644 --- a/nats/loopback.go +++ b/natsclient_loopback.go @@ -19,57 +19,36 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package nats +package signaling import ( "container/list" - "context" "encoding/json" + "log" "strings" "sync" "github.com/nats-io/nats.go" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) -type LoopbackClient struct { - logger log.Logger +type LoopbackNatsClient struct { + mu sync.Mutex + subscriptions map[string]map[*loopbackNatsSubscription]bool - mu sync.Mutex - closed chan struct{} - - // +checklocks:mu - subscriptions map[string]map[*loopbackSubscription]bool - - // +checklocks:mu - wakeup sync.Cond - // +checklocks:mu + wakeup sync.Cond incoming list.List } -func NewLoopbackClient(logger log.Logger) (Client, error) { - client := &LoopbackClient{ - logger: logger, - closed: make(chan struct{}), - - subscriptions: make(map[string]map[*loopbackSubscription]bool), +func NewLoopbackNatsClient() (NatsClient, error) { + client := &LoopbackNatsClient{ + subscriptions: make(map[string]map[*loopbackNatsSubscription]bool), } client.wakeup.L = &client.mu go client.processMessages() return client, nil } -func (c *LoopbackClient) SubscriptionCount() int { - c.mu.Lock() - defer c.mu.Unlock() - - return len(c.subscriptions) -} - -func (c *LoopbackClient) processMessages() { - defer close(c.closed) - +func (c *LoopbackNatsClient) processMessages() { c.mu.Lock() defer c.mu.Unlock() for { @@ -81,19 +60,18 @@ func (c *LoopbackClient) processMessages() { break } - msg := c.incoming.Remove(c.incoming.Front()).(*Msg) + msg := c.incoming.Remove(c.incoming.Front()).(*nats.Msg) c.processMessage(msg) } } -// +checklocks:c.mu -func (c *LoopbackClient) processMessage(msg *Msg) { +func (c *LoopbackNatsClient) processMessage(msg *nats.Msg) { subs, found := c.subscriptions[msg.Subject] if !found { return } - channels := make([]chan *Msg, 0, len(subs)) + channels := make([]chan *nats.Msg, 0, len(subs)) for sub := range subs { channels = append(channels, sub.ch) } @@ -103,12 +81,12 @@ func (c *LoopbackClient) processMessage(msg *Msg) { select { case ch <- msg: default: - c.logger.Printf("Slow consumer %s, dropping message", msg.Subject) + log.Printf("Slow consumer %s, dropping message", msg.Subject) } } } -func (c *LoopbackClient) doClose() { +func (c *LoopbackNatsClient) Close() { c.mu.Lock() defer c.mu.Unlock() @@ -117,29 +95,19 @@ func (c *LoopbackClient) doClose() { c.wakeup.Signal() } -func (c *LoopbackClient) Close(ctx context.Context) error { - c.doClose() - select { - case <-c.closed: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -type loopbackSubscription struct { +type loopbackNatsSubscription struct { subject string - client *LoopbackClient + client *LoopbackNatsClient - ch chan *Msg + ch chan *nats.Msg } -func (s *loopbackSubscription) Unsubscribe() error { +func (s *loopbackNatsSubscription) Unsubscribe() error { s.client.unsubscribe(s) return nil } -func (c *LoopbackClient) Subscribe(subject string, ch chan *Msg) (Subscription, error) { +func (c *LoopbackNatsClient) Subscribe(subject string, ch chan *nats.Msg) (NatsSubscription, error) { if strings.HasSuffix(subject, ".") || strings.Contains(subject, " ") { return nil, nats.ErrBadSubject } @@ -150,14 +118,14 @@ func (c *LoopbackClient) Subscribe(subject string, ch chan *Msg) (Subscription, return nil, nats.ErrConnectionClosed } - s := &loopbackSubscription{ + s := &loopbackNatsSubscription{ subject: subject, client: c, ch: ch, } subs, found := c.subscriptions[subject] if !found { - subs = make(map[*loopbackSubscription]bool) + subs = make(map[*loopbackNatsSubscription]bool) c.subscriptions[subject] = subs } subs[s] = true @@ -165,7 +133,7 @@ func (c *LoopbackClient) Subscribe(subject string, ch chan *Msg) (Subscription, return s, nil } -func (c *LoopbackClient) unsubscribe(s *loopbackSubscription) { +func (c *LoopbackNatsClient) unsubscribe(s *loopbackNatsSubscription) { c.mu.Lock() defer c.mu.Unlock() @@ -177,7 +145,7 @@ func (c *LoopbackClient) unsubscribe(s *loopbackSubscription) { } } -func (c *LoopbackClient) Publish(subject string, message any) error { +func (c *LoopbackNatsClient) Publish(subject string, message interface{}) error { if strings.HasSuffix(subject, ".") || strings.Contains(subject, " ") { return nats.ErrBadSubject } @@ -188,7 +156,7 @@ func (c *LoopbackClient) Publish(subject string, message any) error { return nats.ErrConnectionClosed } - msg := &Msg{ + msg := &nats.Msg{ Subject: subject, } var err error @@ -199,3 +167,7 @@ func (c *LoopbackClient) Publish(subject string, message any) error { c.wakeup.Signal() return nil } + +func (c *LoopbackNatsClient) Decode(msg *nats.Msg, v interface{}) error { + return json.Unmarshal(msg.Data, v) +} diff --git a/nats/loopback_test.go b/natsclient_loopback_test.go similarity index 51% rename from nats/loopback_test.go rename to natsclient_loopback_test.go index f839e32..d6cf5de 100644 --- a/nats/loopback_test.go +++ b/natsclient_loopback_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package nats +package signaling import ( "context" @@ -28,46 +28,66 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" ) -func CreateLoopbackClientForTest(t *testing.T) Client { - logger := logtest.NewLoggerForTest(t) - result, err := NewLoopbackClient(logger) +func (c *LoopbackNatsClient) waitForSubscriptionsEmpty(ctx context.Context, t *testing.T) { + for { + c.mu.Lock() + count := len(c.subscriptions) + c.mu.Unlock() + if count == 0 { + break + } + + select { + case <-ctx.Done(): + c.mu.Lock() + assert.NoError(t, ctx.Err(), "Error waiting for subscriptions %+v to terminate", c.subscriptions) + c.mu.Unlock() + return + default: + time.Sleep(time.Millisecond) + } + } +} + +func CreateLoopbackNatsClientForTest(t *testing.T) NatsClient { + result, err := NewLoopbackNatsClient() require.NoError(t, err) t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - assert.NoError(t, result.Close(ctx)) + result.Close() }) return result } -func TestLoopbackClient_Subscribe(t *testing.T) { - t.Parallel() +func TestLoopbackNatsClient_Subscribe(t *testing.T) { + ensureNoGoroutinesLeak(t, func(t *testing.T) { + client := CreateLoopbackNatsClientForTest(t) - client := CreateLoopbackClientForTest(t) - testClient_Subscribe(t, client) + testNatsClient_Subscribe(t, client) + }) } func TestLoopbackClient_PublishAfterClose(t *testing.T) { - t.Parallel() + ensureNoGoroutinesLeak(t, func(t *testing.T) { + client := CreateLoopbackNatsClientForTest(t) - client := CreateLoopbackClientForTest(t) - test_PublishAfterClose(t, client) + testNatsClient_PublishAfterClose(t, client) + }) } func TestLoopbackClient_SubscribeAfterClose(t *testing.T) { - t.Parallel() + ensureNoGoroutinesLeak(t, func(t *testing.T) { + client := CreateLoopbackNatsClientForTest(t) - client := CreateLoopbackClientForTest(t) - testClient_SubscribeAfterClose(t, client) + testNatsClient_SubscribeAfterClose(t, client) + }) } func TestLoopbackClient_BadSubjects(t *testing.T) { - t.Parallel() + ensureNoGoroutinesLeak(t, func(t *testing.T) { + client := CreateLoopbackNatsClientForTest(t) - client := CreateLoopbackClientForTest(t) - testClient_BadSubjects(t, client) + testNatsClient_BadSubjects(t, client) + }) } diff --git a/natsclient_test.go b/natsclient_test.go new file mode 100644 index 0000000..430ef6d --- /dev/null +++ b/natsclient_test.go @@ -0,0 +1,162 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2021 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/nats-io/nats.go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + natsserver "github.com/nats-io/nats-server/v2/test" +) + +func startLocalNatsServer(t *testing.T) string { + opts := natsserver.DefaultTestOptions + opts.Port = -1 + opts.Cluster.Name = "testing" + srv := natsserver.RunServer(&opts) + t.Cleanup(func() { + srv.Shutdown() + srv.WaitForShutdown() + }) + return srv.ClientURL() +} + +func CreateLocalNatsClientForTest(t *testing.T) NatsClient { + url := startLocalNatsServer(t) + result, err := NewNatsClient(url) + require.NoError(t, err) + t.Cleanup(func() { + result.Close() + }) + return result +} + +func testNatsClient_Subscribe(t *testing.T, client NatsClient) { + require := require.New(t) + assert := assert.New(t) + dest := make(chan *nats.Msg) + sub, err := client.Subscribe("foo", dest) + require.NoError(err) + ch := make(chan struct{}) + + var received atomic.Int32 + maxPublish := int32(20) + ready := make(chan struct{}) + quit := make(chan struct{}) + defer close(quit) + go func() { + close(ready) + for { + select { + case <-dest: + total := received.Add(1) + if total == maxPublish { + if err := sub.Unsubscribe(); !assert.NoError(err) { + return + } + close(ch) + } + case <-quit: + return + } + } + }() + <-ready + for i := int32(0); i < maxPublish; i++ { + assert.NoError(client.Publish("foo", []byte("hello"))) + + // Allow NATS goroutines to process messages. + time.Sleep(10 * time.Millisecond) + } + <-ch + + require.EqualValues(maxPublish, received.Load(), "Received wrong # of messages") +} + +func TestNatsClient_Subscribe(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { + client := CreateLocalNatsClientForTest(t) + + testNatsClient_Subscribe(t, client) + }) +} + +func testNatsClient_PublishAfterClose(t *testing.T, client NatsClient) { + client.Close() + + assert.ErrorIs(t, client.Publish("foo", "bar"), nats.ErrConnectionClosed) +} + +func TestNatsClient_PublishAfterClose(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { + client := CreateLocalNatsClientForTest(t) + + testNatsClient_PublishAfterClose(t, client) + }) +} + +func testNatsClient_SubscribeAfterClose(t *testing.T, client NatsClient) { + client.Close() + + ch := make(chan *nats.Msg) + _, err := client.Subscribe("foo", ch) + assert.ErrorIs(t, err, nats.ErrConnectionClosed) +} + +func TestNatsClient_SubscribeAfterClose(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { + client := CreateLocalNatsClientForTest(t) + + testNatsClient_SubscribeAfterClose(t, client) + }) +} + +func testNatsClient_BadSubjects(t *testing.T, client NatsClient) { + assert := assert.New(t) + subjects := []string{ + "foo bar", + "foo.", + } + + ch := make(chan *nats.Msg) + for _, s := range subjects { + _, err := client.Subscribe(s, ch) + assert.ErrorIs(err, nats.ErrBadSubject, "Expected error for subject %s", s) + } +} + +func TestNatsClient_BadSubjects(t *testing.T) { + CatchLogForTest(t) + ensureNoGoroutinesLeak(t, func(t *testing.T) { + client := CreateLocalNatsClientForTest(t) + + testNatsClient_BadSubjects(t, client) + }) +} diff --git a/async/notifier.go b/notifier.go similarity index 62% rename from async/notifier.go rename to notifier.go index 747ec5e..3466f45 100644 --- a/async/notifier.go +++ b/notifier.go @@ -19,79 +19,60 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package async +package signaling import ( "context" "sync" ) -type rootWaiter struct { - key string - ch chan struct{} -} - -func (w *rootWaiter) notify() { - close(w.ch) -} - type Waiter struct { key string - ch <-chan struct{} + + sw *SingleWaiter } func (w *Waiter) Wait(ctx context.Context) error { - select { - case <-w.ch: - return nil - case <-ctx.Done(): - return ctx.Err() - } + return w.sw.Wait(ctx) } type Notifier struct { sync.Mutex - // +checklocks:Mutex - waiters map[string]*rootWaiter - // +checklocks:Mutex + waiters map[string]*Waiter waiterMap map[string]map[*Waiter]bool } -type ReleaseFunc func() - -func (n *Notifier) NewWaiter(key string) (*Waiter, ReleaseFunc) { +func (n *Notifier) NewWaiter(key string) *Waiter { n.Lock() defer n.Unlock() waiter, found := n.waiters[key] - if !found { - waiter = &rootWaiter{ + if found { + w := &Waiter{ key: key, - ch: make(chan struct{}), - } - - if n.waiters == nil { - n.waiters = make(map[string]*rootWaiter) - } - if n.waiterMap == nil { - n.waiterMap = make(map[string]map[*Waiter]bool) - } - n.waiters[key] = waiter - if _, found := n.waiterMap[key]; !found { - n.waiterMap[key] = make(map[*Waiter]bool) + sw: waiter.sw, } + n.waiterMap[key][w] = true + return w } - w := &Waiter{ + waiter = &Waiter{ key: key, - ch: waiter.ch, + sw: newSingleWaiter(), } - n.waiterMap[key][w] = true - releaseFunc := func() { - n.release(w) + if n.waiters == nil { + n.waiters = make(map[string]*Waiter) } - return w, releaseFunc + if n.waiterMap == nil { + n.waiterMap = make(map[string]map[*Waiter]bool) + } + n.waiters[key] = waiter + if _, found := n.waiterMap[key]; !found { + n.waiterMap[key] = make(map[*Waiter]bool) + } + n.waiterMap[key][waiter] = true + return waiter } func (n *Notifier) Reset() { @@ -99,13 +80,13 @@ func (n *Notifier) Reset() { defer n.Unlock() for _, w := range n.waiters { - w.notify() + w.sw.cancel() } n.waiters = nil n.waiterMap = nil } -func (n *Notifier) release(w *Waiter) { +func (n *Notifier) Release(w *Waiter) { n.Lock() defer n.Unlock() @@ -113,10 +94,8 @@ func (n *Notifier) release(w *Waiter) { if _, found := waiters[w]; found { delete(waiters, w) if len(waiters) == 0 { - if root, found := n.waiters[w.key]; found { - delete(n.waiters, w.key) - root.notify() - } + delete(n.waiters, w.key) + w.sw.cancel() } } } @@ -127,7 +106,7 @@ func (n *Notifier) Notify(key string) { defer n.Unlock() if w, found := n.waiters[key]; found { - w.notify() + w.sw.cancel() delete(n.waiters, w.key) delete(n.waiterMap, w.key) } diff --git a/async/notifier_test.go b/notifier_test.go similarity index 60% rename from async/notifier_test.go rename to notifier_test.go index 74f88ed..c40d7f0 100644 --- a/async/notifier_test.go +++ b/notifier_test.go @@ -19,76 +19,49 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package async +package signaling import ( "context" "sync" "testing" - "testing/synctest" "time" "github.com/stretchr/testify/assert" ) func TestNotifierNoWaiter(t *testing.T) { - t.Parallel() var notifier Notifier // Notifications can be sent even if no waiter exists. notifier.Notify("foo") } -func TestNotifierWaitTimeout(t *testing.T) { - t.Parallel() - synctest.Test(t, func(t *testing.T) { - var notifier Notifier - - notified := make(chan struct{}) - go func() { - defer close(notified) - time.Sleep(time.Second) - notifier.Notify("foo") - }() - - ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) - defer cancel() - - waiter, release := notifier.NewWaiter("foo") - defer release() - - err := waiter.Wait(ctx) - assert.ErrorIs(t, err, context.DeadlineExceeded) - <-notified - - assert.NoError(t, waiter.Wait(t.Context())) - }) -} - func TestNotifierSimple(t *testing.T) { - t.Parallel() - var notifier Notifier - waiter, release := notifier.NewWaiter("foo") - defer release() var wg sync.WaitGroup - wg.Go(func() { + wg.Add(1) + + waiter := notifier.NewWaiter("foo") + defer notifier.Release(waiter) + + go func() { + defer wg.Done() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() assert.NoError(t, waiter.Wait(ctx)) - }) + }() notifier.Notify("foo") wg.Wait() } func TestNotifierMultiNotify(t *testing.T) { - t.Parallel() var notifier Notifier - _, release := notifier.NewWaiter("foo") - defer release() + waiter := notifier.NewWaiter("foo") + defer notifier.Release(waiter) notifier.Notify("foo") // The second notification will be ignored while the first is still pending. @@ -96,41 +69,41 @@ func TestNotifierMultiNotify(t *testing.T) { } func TestNotifierWaitClosed(t *testing.T) { - t.Parallel() var notifier Notifier - waiter, release := notifier.NewWaiter("foo") - release() + waiter := notifier.NewWaiter("foo") + notifier.Release(waiter) assert.NoError(t, waiter.Wait(context.Background())) } func TestNotifierWaitClosedMulti(t *testing.T) { - t.Parallel() var notifier Notifier - waiter1, release1 := notifier.NewWaiter("foo") - waiter2, release2 := notifier.NewWaiter("foo") - release1() - release2() + waiter1 := notifier.NewWaiter("foo") + waiter2 := notifier.NewWaiter("foo") + notifier.Release(waiter1) + notifier.Release(waiter2) assert.NoError(t, waiter1.Wait(context.Background())) assert.NoError(t, waiter2.Wait(context.Background())) } func TestNotifierResetWillNotify(t *testing.T) { - t.Parallel() - var notifier Notifier - waiter, release := notifier.NewWaiter("foo") - defer release() var wg sync.WaitGroup - wg.Go(func() { + wg.Add(1) + + waiter := notifier.NewWaiter("foo") + defer notifier.Release(waiter) + + go func() { + defer wg.Done() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() assert.NoError(t, waiter.Wait(ctx)) - }) + }() notifier.Reset() wg.Wait() @@ -138,24 +111,31 @@ func TestNotifierResetWillNotify(t *testing.T) { func TestNotifierDuplicate(t *testing.T) { t.Parallel() - synctest.Test(t, func(t *testing.T) { - var notifier Notifier - var done sync.WaitGroup + var notifier Notifier + var wgStart sync.WaitGroup + var wgEnd sync.WaitGroup - for range 2 { - done.Go(func() { - waiter, release := notifier.NewWaiter("foo") - defer release() + for i := 0; i < 2; i++ { + wgStart.Add(1) + wgEnd.Add(1) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - assert.NoError(t, waiter.Wait(ctx)) - }) - } + go func() { + defer wgEnd.Done() + waiter := notifier.NewWaiter("foo") + defer notifier.Release(waiter) - synctest.Wait() + // Goroutine has created the waiter and is ready. + wgStart.Done() - notifier.Notify("foo") - done.Wait() - }) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + assert.NoError(t, waiter.Wait(ctx)) + }() + } + + wgStart.Wait() + + time.Sleep(100 * time.Millisecond) + notifier.Notify("foo") + wgEnd.Wait() } diff --git a/pool/buffer_pool.go b/pool/buffer_pool.go deleted file mode 100644 index 8164bc5..0000000 --- a/pool/buffer_pool.go +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2024 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package pool - -import ( - "bytes" - "encoding/json" - "io" - "sync" -) - -type BufferPool struct { - buffers sync.Pool - copyBuffers sync.Pool -} - -func (p *BufferPool) Get() *bytes.Buffer { - b := p.buffers.Get() - if b == nil { - return bytes.NewBuffer(nil) - } - - return b.(*bytes.Buffer) -} - -func (p *BufferPool) Put(b *bytes.Buffer) { - if b == nil { - return - } - - b.Reset() - p.buffers.Put(b) -} - -func (p *BufferPool) ReadAll(r io.Reader) (*bytes.Buffer, error) { - buf := p.copyBuffers.Get() - if buf == nil { - buf = make([]byte, 1024) - } - defer p.copyBuffers.Put(buf) - - b := p.Get() - if _, err := io.CopyBuffer(b, r, buf.([]byte)); err != nil { - p.Put(b) - return nil, err - } - - return b, nil -} - -func (p *BufferPool) MarshalAsJSON(v any) (*bytes.Buffer, error) { - b := p.Get() - encoder := json.NewEncoder(b) - if err := encoder.Encode(v); err != nil { - p.Put(b) - return nil, err - } - - return b, nil -} diff --git a/pool/buffer_pool_test.go b/pool/buffer_pool_test.go deleted file mode 100644 index f06b969..0000000 --- a/pool/buffer_pool_test.go +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package pool - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBufferPool(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - - var pool BufferPool - - buf1 := pool.Get() - assert.NotNil(buf1) - buf2 := pool.Get() - assert.NotSame(buf1, buf2) - - buf1.WriteString("empty string") - pool.Put(buf1) - - buf3 := pool.Get() - assert.Equal(0, buf3.Len()) - - pool.Put(nil) -} - -func TestBufferPoolReadAll(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - s := "Hello world!" - data := bytes.NewBufferString(s) - - var pool BufferPool - - buf1 := pool.Get() - assert.NotNil(buf1) - pool.Put(buf1) - - buf2, err := pool.ReadAll(data) - require.NoError(err) - assert.Equal(s, buf2.String()) -} - -var errTest = errors.New("test error") - -type errorReader struct{} - -func (e errorReader) Read(b []byte) (int, error) { - return 0, errTest -} - -func TestBufferPoolReadAllError(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - - var pool BufferPool - - buf1 := pool.Get() - assert.NotNil(buf1) - pool.Put(buf1) - - r := &errorReader{} - buf2, err := pool.ReadAll(r) - assert.ErrorIs(err, errTest) - assert.Nil(buf2) -} - -func TestBufferPoolMarshalAsJSON(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - var pool BufferPool - buf1 := pool.Get() - assert.NotNil(buf1) - pool.Put(buf1) - - s := "Hello world!" - buf2, err := pool.MarshalAsJSON(s) - require.NoError(err) - - assert.Equal(fmt.Sprintf("\"%s\"\n", s), buf2.String()) -} - -type errorMarshaler struct { - json.Marshaler -} - -func (e errorMarshaler) MarshalJSON() ([]byte, error) { - return nil, errTest -} - -func TestBufferPoolMarshalAsJSONError(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - - var pool BufferPool - buf1 := pool.Get() - assert.NotNil(buf1) - pool.Put(buf1) - - var ob errorMarshaler - buf2, err := pool.MarshalAsJSON(ob) - assert.ErrorIs(err, errTest) - assert.Nil(buf2) -} diff --git a/proxy.conf.in b/proxy.conf.in index ac3482b..5cbb3c4 100644 --- a/proxy.conf.in +++ b/proxy.conf.in @@ -4,9 +4,7 @@ #listen = 127.0.0.1:9090 [app] -# Set to "true" to install pprof debug handlers. Access will only be possible -# from IPs allowed through the "allowed_ips" option below. -# +# Set to "true" to install pprof debug handlers. # See "https://golang.org/pkg/net/http/pprof/" for further information. #debug = false @@ -84,17 +82,9 @@ url = ws://localhost:8188/ # Default is 2 mbit/sec. #maxscreenbitrate = 2097152 -# List of IP addresses / subnets that are allowed to be used by clients in -# candidates. The allowed list has preference over the blocked list below. -#allowedcandidates = 10.0.0.0/8 - -# List of IP addresses / subnets to filter from candidates received by clients. -#blockedcandidates = 1.2.3.0/24 - [stats] -# Comma-separated list of IP addresses that are allowed to access the debug, -# stats and metrics endpoints. -# Leave empty (or commented) to only allow access from localhost. +# Comma-separated list of IP addresses that are allowed to access the stats +# endpoint. Leave empty (or commented) to only allow access from "127.0.0.1". #allowed_ips = [etcd] diff --git a/proxy/api_test.go b/proxy/api_test.go deleted file mode 100644 index 873ce32..0000000 --- a/proxy/api_test.go +++ /dev/null @@ -1,591 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package proxy - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" -) - -func TestValidate(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - testcases := []struct { - message *ClientMessage - reason string - }{ - { - &ClientMessage{}, - "type missing", - }, - { - // Unknown types are ignored. - &ClientMessage{ - Type: "invalid", - }, - "", - }, - { - &ClientMessage{ - Type: "hello", - }, - "hello missing", - }, - { - &ClientMessage{ - Type: "hello", - Hello: &HelloClientMessage{}, - }, - "unsupported hello version", - }, - { - &ClientMessage{ - Type: "hello", - Hello: &HelloClientMessage{ - Version: "abc", - }, - }, - "unsupported hello version", - }, - { - &ClientMessage{ - Type: "hello", - Hello: &HelloClientMessage{ - Version: "1.0", - }, - }, - "token missing", - }, - { - &ClientMessage{ - Type: "hello", - Hello: &HelloClientMessage{ - Version: "1.0", - Token: "token", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "hello", - Hello: &HelloClientMessage{ - Version: "1.0", - ResumeId: "resume-id", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "bye", - }, - "", - }, - { - &ClientMessage{ - Type: "bye", - Bye: &ByeClientMessage{}, - }, - "", - }, - { - &ClientMessage{ - Type: "command", - }, - "command missing", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{}, - }, - "type missing", - }, - { - // Unknown types are ignored. - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "invalid", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "create-publisher", - }, - }, - "stream type missing", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "create-publisher", - StreamType: sfu.StreamTypeVideo, - }, - }, - "", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "create-subscriber", - }, - }, - "publisher id missing", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "create-subscriber", - PublisherId: "foo", - }, - }, - "stream type missing", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "create-subscriber", - PublisherId: "foo", - StreamType: sfu.StreamTypeVideo, - }, - }, - "", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "create-subscriber", - PublisherId: "foo", - StreamType: sfu.StreamTypeVideo, - RemoteUrl: "http://domain.invalid", - }, - }, - "remote token missing", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "create-subscriber", - PublisherId: "foo", - StreamType: sfu.StreamTypeVideo, - RemoteUrl: ":", - RemoteToken: "remote-token", - }, - }, - "invalid remote url", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "create-subscriber", - PublisherId: "foo", - StreamType: sfu.StreamTypeVideo, - RemoteUrl: "http://domain.invalid", - RemoteToken: "remote-token", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "delete-publisher", - }, - }, - "client id missing", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "delete-publisher", - ClientId: "foo", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "delete-subscriber", - }, - }, - "client id missing", - }, - { - &ClientMessage{ - Type: "command", - Command: &CommandClientMessage{ - Type: "delete-subscriber", - ClientId: "foo", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "payload", - }, - "payload missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{}, - }, - "type missing", - }, - { - // Unknown types are ignored. - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "invalid", - }, - }, - "client id missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "offer", - }, - }, - "payload missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "offer", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - }, - }, - "client id missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "offer", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - ClientId: "foo", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "answer", - }, - }, - "payload missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "answer", - Payload: api.StringMap{ - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, - }, - "client id missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "answer", - Payload: api.StringMap{ - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - ClientId: "foo", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "candidate", - }, - }, - "payload missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "candidate", - Payload: api.StringMap{ - "candidate": "invalid-candidate", - }, - }, - }, - "client id missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "candidate", - Payload: api.StringMap{ - "candidate": "invalid-candidate", - }, - ClientId: "foo", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "endOfCandidates", - }, - }, - "client id missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "endOfCandidates", - ClientId: "foo", - }, - }, - "", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "requestoffer", - }, - }, - "client id missing", - }, - { - &ClientMessage{ - Type: "payload", - Payload: &PayloadClientMessage{ - Type: "requestoffer", - ClientId: "foo", - }, - }, - "", - }, - } - - for idx, tc := range testcases { - err := tc.message.CheckValid() - if tc.reason == "" { - assert.NoError(err, "failed for testcase %d: %+v", idx, tc.message) - } else { - assert.ErrorContains(err, tc.reason, "failed for testcase %d: %+v", idx, tc.message) - } - } -} - -func TestServerErrorMessage(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - message := &ClientMessage{ - Id: "12346", - } - err := message.NewErrorServerMessage(api.NewError("error_code", "Test error")) - assert.Equal(message.Id, err.Id) - if e := err.Error; assert.NotNil(e) { - assert.Equal("error_code", e.Code) - assert.Equal("Test error", e.Message) - assert.Empty(e.Details) - } -} - -func TestWrapperServerErrorMessage(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - message := &ClientMessage{ - Id: "12346", - } - err := message.NewWrappedErrorServerMessage(errors.New("an internal server error")) - assert.Equal(message.Id, err.Id) - if e := err.Error; assert.NotNil(e) { - assert.Equal("internal_error", e.Code) - assert.Equal("an internal server error", e.Message) - assert.Empty(e.Details) - } -} - -func TestCloseAfterSend(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - message := &ServerMessage{ - Type: "bye", - } - assert.True(message.CloseAfterSend(nil)) - - for _, msgType := range []string{ - "error", - "hello", - "command", - "payload", - "event", - } { - message = &ServerMessage{ - Type: msgType, - } - assert.False(message.CloseAfterSend(nil), "failed for %s", msgType) - } -} - -func TestAllowIncoming(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - testcases := []struct { - bw float64 - allow bool - }{ - { - 0, true, - }, - { - 99, true, - }, - { - 99.9, true, - }, - { - 100, false, - }, - { - 200, false, - }, - } - - bw := EventServerBandwidth{ - Incoming: nil, - } - assert.True(bw.AllowIncoming()) - for idx, tc := range testcases { - bw := EventServerBandwidth{ - Incoming: internal.MakePtr(tc.bw), - } - assert.Equal(tc.allow, bw.AllowIncoming(), "failed for testcase %d: %+v", idx, tc) - } -} - -func TestAllowOutgoing(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - testcases := []struct { - bw float64 - allow bool - }{ - { - 0, true, - }, - { - 99, true, - }, - { - 99.9, true, - }, - { - 100, false, - }, - { - 200, false, - }, - } - - bw := EventServerBandwidth{ - Outgoing: nil, - } - assert.True(bw.AllowOutgoing()) - for idx, tc := range testcases { - bw := EventServerBandwidth{ - Outgoing: internal.MakePtr(tc.bw), - } - assert.Equal(tc.allow, bw.AllowOutgoing(), "failed for testcase %d: %+v", idx, tc) - } -} - -func TestInformationEtcd(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - - info1 := &InformationEtcd{} - assert.ErrorContains(info1.CheckValid(), "address missing") - - info2 := &InformationEtcd{ - Address: "http://domain.invalid", - } - if assert.NoError(info2.CheckValid()) { - assert.Equal("http://domain.invalid/", info2.Address) - } - - info3 := &InformationEtcd{ - Address: "http://domain.invalid/", - } - if assert.NoError(info3.CheckValid()) { - assert.Equal("http://domain.invalid/", info3.Address) - } -} diff --git a/cmd/proxy/main.go b/proxy/main.go similarity index 62% rename from cmd/proxy/main.go rename to proxy/main.go index 7d1ecfc..18f7bf5 100644 --- a/cmd/proxy/main.go +++ b/proxy/main.go @@ -22,7 +22,6 @@ package main import ( - "context" "flag" "fmt" "log" @@ -31,15 +30,14 @@ import ( "os" "os/signal" "runtime" + "strings" "syscall" "time" "github.com/dlintw/goconf" "github.com/gorilla/mux" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - signalinglog "github.com/strukturag/nextcloud-spreed-signaling/v2/log" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) var ( @@ -67,52 +65,49 @@ func main() { } sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt) signal.Notify(sigChan, syscall.SIGHUP) signal.Notify(sigChan, syscall.SIGUSR1) - stopCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() + log.Printf("Starting up version %s/%s as pid %d", version, runtime.Version(), os.Getpid()) - logger := log.Default() - stopCtx = signalinglog.NewLoggerContext(stopCtx, logger) - - logger.Printf("Starting up version %s/%s as pid %d", version, runtime.Version(), os.Getpid()) - - cfg, err := goconf.ReadConfigFile(*configFlag) + config, err := goconf.ReadConfigFile(*configFlag) if err != nil { - logger.Fatal("Could not read configuration: ", err) + log.Fatal("Could not read configuration: ", err) } - logger.Printf("Using a maximum of %d CPUs", runtime.GOMAXPROCS(0)) + cpus := runtime.NumCPU() + runtime.GOMAXPROCS(cpus) + log.Printf("Using a maximum of %d CPUs", cpus) r := mux.NewRouter() - proxy, err := NewProxyServer(stopCtx, r, version, cfg) + proxy, err := NewProxyServer(r, version, config) if err != nil { - logger.Fatal(err) + log.Fatal(err) } - if err := proxy.Start(cfg); err != nil { - logger.Fatal(err) + if err := proxy.Start(config); err != nil { + log.Fatal(err) } defer proxy.Stop() - if addr, _ := config.GetStringOptionWithEnv(cfg, "http", "listen"); addr != "" { - readTimeout, _ := cfg.GetInt("http", "readtimeout") + if addr, _ := signaling.GetStringOptionWithEnv(config, "http", "listen"); addr != "" { + readTimeout, _ := config.GetInt("http", "readtimeout") if readTimeout <= 0 { readTimeout = defaultReadTimeout } - writeTimeout, _ := cfg.GetInt("http", "writetimeout") + writeTimeout, _ := config.GetInt("http", "writetimeout") if writeTimeout <= 0 { writeTimeout = defaultWriteTimeout } - for address := range internal.SplitEntries(addr, " ") { + for _, address := range strings.Split(addr, " ") { go func(address string) { - logger.Println("Listening on", address) + log.Println("Listening on", address) listener, err := net.Listen("tcp", address) if err != nil { - logger.Fatal("Could not start listening: ", err) + log.Fatal("Could not start listening: ", err) } srv := &http.Server{ Handler: r, @@ -122,7 +117,7 @@ func main() { WriteTimeout: time.Duration(writeTimeout) * time.Second, } if err := srv.Serve(listener); err != nil { - logger.Fatal("Could not start server: ", err) + log.Fatal("Could not start server: ", err) } }(address) } @@ -131,24 +126,24 @@ func main() { loop: for { select { - case <-stopCtx.Done(): - logger.Println("Interrupted") - break loop case sig := <-sigChan: switch sig { + case os.Interrupt: + log.Println("Interrupted") + break loop case syscall.SIGHUP: - logger.Printf("Received SIGHUP, reloading %s", *configFlag) + log.Printf("Received SIGHUP, reloading %s", *configFlag) if config, err := goconf.ReadConfigFile(*configFlag); err != nil { - logger.Printf("Could not read configuration from %s: %s", *configFlag, err) + log.Printf("Could not read configuration from %s: %s", *configFlag, err) } else { proxy.Reload(config) } case syscall.SIGUSR1: - logger.Printf("Received SIGUSR1, scheduling server to shutdown") + log.Printf("Received SIGUSR1, scheduling server to shutdown") proxy.ScheduleShutdown() } case <-proxy.ShutdownChannel(): - logger.Printf("All clients disconnected, shutting down") + log.Printf("All clients disconnected, shutting down") break loop } } diff --git a/cmd/proxy/proxy_client.go b/proxy/proxy_client.go similarity index 72% rename from cmd/proxy/proxy_client.go rename to proxy/proxy_client.go index 1a16670..935a2b9 100644 --- a/cmd/proxy/proxy_client.go +++ b/proxy/proxy_client.go @@ -27,35 +27,25 @@ import ( "time" "github.com/gorilla/websocket" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/client" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) type ProxyClient struct { - client.Client + signaling.Client proxy *ProxyServer session atomic.Pointer[ProxySession] } -func NewProxyClient(ctx context.Context, proxy *ProxyServer, conn *websocket.Conn, addr string, agent string) (*ProxyClient, error) { +func NewProxyClient(ctx context.Context, proxy *ProxyServer, conn *websocket.Conn, addr string) (*ProxyClient, error) { client := &ProxyClient{ proxy: proxy, } - client.SetConn(ctx, conn, addr, agent, false, client) + client.SetConn(ctx, conn, addr, client) return client, nil } -func (c *ProxyClient) GetSessionId() api.PublicSessionId { - if session := c.GetSession(); session != nil { - return session.PublicId() - } - - return "" -} - func (c *ProxyClient) GetSession() *ProxySession { return c.session.Load() } @@ -64,18 +54,18 @@ func (c *ProxyClient) SetSession(session *ProxySession) { c.session.Store(session) } -func (c *ProxyClient) OnClosed() { - if session := c.session.Swap(nil); session != nil { +func (c *ProxyClient) OnClosed(client signaling.HandlerClient) { + if session := c.GetSession(); session != nil { session.MarkUsed() } - c.proxy.clientClosed(c) + c.proxy.clientClosed(&c.Client) } -func (c *ProxyClient) OnMessageReceived(data []byte) { +func (c *ProxyClient) OnMessageReceived(client signaling.HandlerClient, data []byte) { c.proxy.processMessage(c, data) } -func (c *ProxyClient) OnRTTReceived(rtt time.Duration) { +func (c *ProxyClient) OnRTTReceived(client signaling.HandlerClient, rtt time.Duration) { if session := c.GetSession(); session != nil { session.MarkUsed() } diff --git a/cmd/proxy/proxy_remote.go b/proxy/proxy_remote.go similarity index 52% rename from cmd/proxy/proxy_remote.go rename to proxy/proxy_remote.go index d6092f3..81a7bbf 100644 --- a/cmd/proxy/proxy_remote.go +++ b/proxy/proxy_remote.go @@ -27,8 +27,7 @@ import ( "crypto/tls" "encoding/json" "errors" - "math/rand/v2" - "net" + "log" "net/http" "net/url" "strconv" @@ -39,15 +38,12 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/gorilla/websocket" - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) const ( initialReconnectInterval = 1 * time.Second - maxReconnectInterval = 16 * time.Second + maxReconnectInterval = 32 * time.Second // Time allowed to write a message to the peer. writeWait = 10 * time.Second @@ -60,56 +56,41 @@ const ( ) var ( - ErrNotConnected = errors.New("not connected") // +checklocksignore: Global readonly variable. + ErrNotConnected = errors.New("not connected") ) type RemoteConnection struct { - logger log.Logger mu sync.Mutex - p *ProxyServer url *url.URL - // +checklocks:mu - conn *websocket.Conn - closeCtx context.Context - closeFunc context.CancelFunc // +checklocksignore: Only written to from constructor. + conn *websocket.Conn + closer *signaling.Closer + closed atomic.Bool tokenId string tokenKey *rsa.PrivateKey tlsConfig *tls.Config - // +checklocks:mu connectedSince time.Time reconnectTimer *time.Timer reconnectInterval atomic.Int64 - msgId atomic.Int64 - // +checklocks:mu + msgId atomic.Int64 helloMsgId string - // +checklocks:mu - sessionId api.PublicSessionId - // +checklocks:mu - helloReceived bool + sessionId string - // +checklocks:mu - pendingMessages []*proxy.ClientMessage - // +checklocks:mu - messageCallbacks map[string]chan *proxy.ServerMessage + pendingMessages []*signaling.ProxyClientMessage + messageCallbacks map[string]chan *signaling.ProxyServerMessage } -func NewRemoteConnection(p *ProxyServer, proxyUrl string, tokenId string, tokenKey *rsa.PrivateKey, tlsConfig *tls.Config) (*RemoteConnection, error) { +func NewRemoteConnection(proxyUrl string, tokenId string, tokenKey *rsa.PrivateKey, tlsConfig *tls.Config) (*RemoteConnection, error) { u, err := url.Parse(proxyUrl) if err != nil { return nil, err } - closeCtx, closeFunc := context.WithCancel(context.Background()) - result := &RemoteConnection{ - logger: p.logger, - p: p, - url: u, - closeCtx: closeCtx, - closeFunc: closeFunc, + url: u, + closer: signaling.NewCloser(), tokenId: tokenId, tokenKey: tokenKey, @@ -117,7 +98,7 @@ func NewRemoteConnection(p *ProxyServer, proxyUrl string, tokenId string, tokenK reconnectTimer: time.NewTimer(0), - messageCallbacks: make(map[string]chan *proxy.ServerMessage), + messageCallbacks: make(map[string]chan *signaling.ProxyServerMessage), } result.reconnectInterval.Store(int64(initialReconnectInterval)) @@ -130,23 +111,16 @@ func (c *RemoteConnection) String() string { return c.url.String() } -func (c *RemoteConnection) SessionId() api.PublicSessionId { - c.mu.Lock() - defer c.mu.Unlock() - return c.sessionId -} - func (c *RemoteConnection) reconnect() { u, err := c.url.Parse("proxy") if err != nil { - c.logger.Printf("Could not resolve url to proxy at %s: %s", c, err) + log.Printf("Could not resolve url to proxy at %s: %s", c, err) c.scheduleReconnect() return } - switch u.Scheme { - case "http": + if u.Scheme == "http" { u.Scheme = "ws" - case "https": + } else if u.Scheme == "https" { u.Scheme = "wss" } @@ -155,81 +129,58 @@ func (c *RemoteConnection) reconnect() { TLSClientConfig: c.tlsConfig, } - conn, _, err := dialer.DialContext(c.closeCtx, u.String(), nil) + conn, _, err := dialer.DialContext(context.TODO(), u.String(), nil) if err != nil { - c.logger.Printf("Error connecting to proxy at %s: %s", c, err) + log.Printf("Error connecting to proxy at %s: %s", c, err) c.scheduleReconnect() return } - c.logger.Printf("Connected to %s", c) + log.Printf("Connected to %s", c) + c.closed.Store(false) c.mu.Lock() - if c.closeCtx.Err() != nil { - // Closed while waiting for lock. - c.mu.Unlock() - if err := conn.Close(); err != nil { - c.logger.Printf("Error closing connection to %s: %s", c, err) - } - return - } c.connectedSince = time.Now() c.conn = conn c.mu.Unlock() c.reconnectInterval.Store(int64(initialReconnectInterval)) - if !c.sendReconnectHello() || !c.sendPing() { + if err := c.sendHello(); err != nil { + log.Printf("Error sending hello request to proxy at %s: %s", c, err) c.scheduleReconnect() return } + if !c.sendPing() { + return + } + go c.readPump(conn) } -func (c *RemoteConnection) sendReconnectHello() bool { - c.mu.Lock() - defer c.mu.Unlock() - - if err := c.sendHello(c.closeCtx); err != nil { - c.logger.Printf("Error sending hello request to proxy at %s: %s", c, err) - return false - } - - return true -} - func (c *RemoteConnection) scheduleReconnect() { - c.mu.Lock() - defer c.mu.Unlock() - - c.scheduleReconnectLocked() -} - -// +checklocks:c.mu -func (c *RemoteConnection) scheduleReconnectLocked() { - if err := c.sendCloseLocked(); err != nil && err != ErrNotConnected { - c.logger.Printf("Could not send close message to %s: %s", c, err) + if err := c.sendClose(); err != nil && err != ErrNotConnected { + log.Printf("Could not send close message to %s: %s", c, err) } - c.closeLocked() + c.close() interval := c.reconnectInterval.Load() - // Prevent all servers from reconnecting at the same time in case of an - // interrupted connection to the proxy or a restart. - jitter := rand.Int64N(interval) - (interval / 2) - c.reconnectTimer.Reset(time.Duration(interval + jitter)) + c.reconnectTimer.Reset(time.Duration(interval)) - interval = min(interval*2, int64(maxReconnectInterval)) + interval = interval * 2 + if interval > int64(maxReconnectInterval) { + interval = int64(maxReconnectInterval) + } c.reconnectInterval.Store(interval) } -// +checklocks:c.mu -func (c *RemoteConnection) sendHello(ctx context.Context) error { +func (c *RemoteConnection) sendHello() error { c.helloMsgId = strconv.FormatInt(c.msgId.Add(1), 10) - msg := &proxy.ClientMessage{ + msg := &signaling.ProxyClientMessage{ Id: c.helloMsgId, Type: "hello", - Hello: &proxy.HelloClientMessage{ + Hello: &signaling.HelloProxyClientMessage{ Version: "1.0", }, } @@ -244,11 +195,13 @@ func (c *RemoteConnection) sendHello(ctx context.Context) error { msg.Hello.Token = tokenString } - return c.sendMessageLocked(ctx, msg) + return c.SendMessage(msg) } -// +checklocks:c.mu -func (c *RemoteConnection) sendCloseLocked() error { +func (c *RemoteConnection) sendClose() error { + c.mu.Lock() + defer c.mu.Unlock() + if c.conn == nil { return ErrNotConnected } @@ -261,39 +214,24 @@ func (c *RemoteConnection) close() { c.mu.Lock() defer c.mu.Unlock() - c.closeLocked() -} - -// +checklocks:c.mu -func (c *RemoteConnection) closeLocked() { if c.conn != nil { c.conn.Close() c.conn = nil } - c.connectedSince = time.Time{} - c.helloReceived = false } func (c *RemoteConnection) Close() error { c.mu.Lock() defer c.mu.Unlock() c.reconnectTimer.Stop() - - if c.closeCtx.Err() != nil { - // Already closed + if c.conn == nil { return nil } - c.closeFunc() - var err1 error - var err2 error - if c.conn != nil { - err1 = c.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Time{}) - err2 = c.conn.Close() - c.conn = nil - } - c.connectedSince = time.Time{} - c.helloReceived = false + c.sendClose() + err1 := c.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Time{}) + err2 := c.conn.Close() + c.conn = nil if err1 != nil { return err1 } @@ -301,7 +239,7 @@ func (c *RemoteConnection) Close() error { } func (c *RemoteConnection) createToken(subject string) (string, error) { - claims := &proxy.TokenClaims{ + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: c.tokenId, @@ -317,15 +255,14 @@ func (c *RemoteConnection) createToken(subject string) (string, error) { return tokenString, nil } -func (c *RemoteConnection) SendMessage(msg *proxy.ClientMessage) error { +func (c *RemoteConnection) SendMessage(msg *signaling.ProxyClientMessage) error { c.mu.Lock() defer c.mu.Unlock() - return c.sendMessageLocked(c.closeCtx, msg) + return c.sendMessageLocked(context.Background(), msg) } -// +checklocks:c.mu -func (c *RemoteConnection) deferMessage(ctx context.Context, msg *proxy.ClientMessage) { +func (c *RemoteConnection) deferMessage(ctx context.Context, msg *signaling.ProxyClientMessage) { c.pendingMessages = append(c.pendingMessages, msg) if ctx.Done() != nil { go func() { @@ -343,8 +280,7 @@ func (c *RemoteConnection) deferMessage(ctx context.Context, msg *proxy.ClientMe } } -// +checklocks:c.mu -func (c *RemoteConnection) sendMessageLocked(ctx context.Context, msg *proxy.ClientMessage) error { +func (c *RemoteConnection) sendMessageLocked(ctx context.Context, msg *signaling.ProxyClientMessage) error { if c.conn == nil { // Defer until connected. c.deferMessage(ctx, msg) @@ -363,7 +299,7 @@ func (c *RemoteConnection) sendMessageLocked(ctx context.Context, msg *proxy.Cli func (c *RemoteConnection) readPump(conn *websocket.Conn) { defer func() { - if c.closeCtx.Err() == nil { + if !c.closed.Load() { c.scheduleReconnect() } }() @@ -378,21 +314,19 @@ func (c *RemoteConnection) readPump(conn *websocket.Conn) { websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { - if !errors.Is(err, net.ErrClosed) || c.closeCtx.Err() == nil { - c.logger.Printf("Error reading from %s: %v", c, err) - } + log.Printf("Error reading from %s: %v", c, err) } break } if msgType != websocket.TextMessage { - c.logger.Printf("unexpected message type %q (%s)", msgType, string(msg)) + log.Printf("unexpected message type %q (%s)", msgType, string(msg)) continue } - var message proxy.ServerMessage + var message signaling.ProxyServerMessage if err := json.Unmarshal(msg, &message); err != nil { - c.logger.Printf("could not decode message %s: %s", string(msg), err) + log.Printf("could not decode message %s: %s", string(msg), err) continue } @@ -419,7 +353,7 @@ func (c *RemoteConnection) sendPing() bool { msg := strconv.FormatInt(now.UnixNano(), 10) c.conn.SetWriteDeadline(now.Add(writeWait)) // nolint if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil { - c.logger.Printf("Could not send ping to proxy at %s: %v", c, err) + log.Printf("Could not send ping to proxy at %s: %v", c, err) go c.scheduleReconnect() return false } @@ -440,48 +374,44 @@ func (c *RemoteConnection) writePump() { c.reconnect() case <-ticker.C: c.sendPing() - case <-c.closeCtx.Done(): + case <-c.closer.C: return } } } -func (c *RemoteConnection) processHello(msg *proxy.ServerMessage) { - c.mu.Lock() - defer c.mu.Unlock() - +func (c *RemoteConnection) processHello(msg *signaling.ProxyServerMessage) { c.helloMsgId = "" switch msg.Type { case "error": if msg.Error.Code == "no_such_session" { - c.logger.Printf("Session %s could not be resumed on %s, registering new", c.sessionId, c) + log.Printf("Session %s could not be resumed on %s, registering new", c.sessionId, c) c.sessionId = "" - if err := c.sendHello(c.closeCtx); err != nil { - c.logger.Printf("Could not send hello request to %s: %s", c, err) - c.scheduleReconnectLocked() + if err := c.sendHello(); err != nil { + log.Printf("Could not send hello request to %s: %s", c, err) + c.scheduleReconnect() } return } - c.logger.Printf("Hello connection to %s failed with %+v, reconnecting", c, msg.Error) - c.scheduleReconnectLocked() + log.Printf("Hello connection to %s failed with %+v, reconnecting", c, msg.Error) + c.scheduleReconnect() case "hello": resumed := c.sessionId == msg.Hello.SessionId c.sessionId = msg.Hello.SessionId - c.helloReceived = true - var country geoip.Country + country := "" if msg.Hello.Server != nil { - if country = msg.Hello.Server.Country; country != "" && !geoip.IsValidCountry(country) { - c.logger.Printf("Proxy %s sent invalid country %s in hello response", c, country) + if country = msg.Hello.Server.Country; country != "" && !signaling.IsValidCountry(country) { + log.Printf("Proxy %s sent invalid country %s in hello response", c, country) country = "" } } if resumed { - c.logger.Printf("Resumed session %s on %s", c.sessionId, c) + log.Printf("Resumed session %s on %s", c.sessionId, c) } else if country != "" { - c.logger.Printf("Received session %s from %s (in %s)", c.sessionId, c, country) + log.Printf("Received session %s from %s (in %s)", c.sessionId, c, country) } else { - c.logger.Printf("Received session %s from %s", c.sessionId, c) + log.Printf("Received session %s from %s", c.sessionId, c) } pending := c.pendingMessages @@ -491,94 +421,60 @@ func (c *RemoteConnection) processHello(msg *proxy.ServerMessage) { continue } - if err := c.sendMessageLocked(c.closeCtx, m); err != nil { - c.logger.Printf("Could not send pending message %+v to %s: %s", m, c, err) + if err := c.sendMessageLocked(context.Background(), m); err != nil { + log.Printf("Could not send pending message %+v to %s: %s", m, c, err) } } default: - c.logger.Printf("Received unsupported hello response %+v from %s, reconnecting", msg, c) - c.scheduleReconnectLocked() + log.Printf("Received unsupported hello response %+v from %s, reconnecting", msg, c) + c.scheduleReconnect() } } -func (c *RemoteConnection) handleCallback(msg *proxy.ServerMessage) bool { - if msg.Id == "" { - return false - } - - c.mu.Lock() - ch, found := c.messageCallbacks[msg.Id] - if !found { +func (c *RemoteConnection) processMessage(msg *signaling.ProxyServerMessage) { + if msg.Id != "" { + c.mu.Lock() + ch, found := c.messageCallbacks[msg.Id] + if found { + delete(c.messageCallbacks, msg.Id) + c.mu.Unlock() + ch <- msg + return + } c.mu.Unlock() - return false - } - - delete(c.messageCallbacks, msg.Id) - c.mu.Unlock() - - ch <- msg - return true -} - -func (c *RemoteConnection) processMessage(msg *proxy.ServerMessage) { - if c.handleCallback(msg) { - return } switch msg.Type { case "event": c.processEvent(msg) - case "bye": - c.logger.Printf("Connection to %s was closed: %s", c, msg.Bye.Reason) - if msg.Bye.Reason == "session_expired" { - // Don't try to resume expired session. - c.mu.Lock() - c.sessionId = "" - c.mu.Unlock() - } - c.scheduleReconnect() default: - c.logger.Printf("Received unsupported message %+v from %s", msg, c) + log.Printf("Received unsupported message %+v from %s", msg, c) } } -func (c *RemoteConnection) processEvent(msg *proxy.ServerMessage) { +func (c *RemoteConnection) processEvent(msg *signaling.ProxyServerMessage) { switch msg.Event.Type { case "update-load": - // Ignore - case "publisher-closed": - c.logger.Printf("Remote publisher %s was closed on %s", msg.Event.ClientId, c) - c.p.RemotePublisherDeleted(api.PublicSessionId(msg.Event.ClientId)) default: - c.logger.Printf("Received unsupported event %+v from %s", msg, c) + log.Printf("Received unsupported event %+v from %s", msg, c) } } -func (c *RemoteConnection) sendMessageWithCallbackLocked(ctx context.Context, msg *proxy.ClientMessage) (string, <-chan *proxy.ServerMessage, error) { +func (c *RemoteConnection) RequestMessage(ctx context.Context, msg *signaling.ProxyClientMessage) (*signaling.ProxyServerMessage, error) { msg.Id = strconv.FormatInt(c.msgId.Add(1), 10) c.mu.Lock() defer c.mu.Unlock() + if err := c.sendMessageLocked(ctx, msg); err != nil { - msg.Id = "" - return "", nil, err - } - - ch := make(chan *proxy.ServerMessage, 1) - c.messageCallbacks[msg.Id] = ch - return msg.Id, ch, nil -} - -func (c *RemoteConnection) RequestMessage(ctx context.Context, msg *proxy.ClientMessage) (*proxy.ServerMessage, error) { - id, ch, err := c.sendMessageWithCallbackLocked(ctx, msg) - if err != nil { return nil, err } - + ch := make(chan *signaling.ProxyServerMessage, 1) + c.messageCallbacks[msg.Id] = ch + c.mu.Unlock() defer func() { c.mu.Lock() - defer c.mu.Unlock() - delete(c.messageCallbacks, id) + delete(c.messageCallbacks, msg.Id) }() select { @@ -592,15 +488,3 @@ func (c *RemoteConnection) RequestMessage(ctx context.Context, msg *proxy.Client return response, nil } } - -func (c *RemoteConnection) SendBye() error { - c.mu.Lock() - defer c.mu.Unlock() - if c.conn == nil { - return nil - } - - return c.sendMessageLocked(c.closeCtx, &proxy.ClientMessage{ - Type: "bye", - }) -} diff --git a/cmd/proxy/proxy_server.go b/proxy/proxy_server.go similarity index 58% rename from cmd/proxy/proxy_server.go rename to proxy/proxy_server.go index 99d42b0..b256d09 100644 --- a/cmd/proxy/proxy_server.go +++ b/proxy/proxy_server.go @@ -30,6 +30,7 @@ import ( "errors" "fmt" "io" + "log" "net" "net/http" "net/http/pprof" @@ -46,21 +47,11 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" "github.com/gorilla/websocket" + "github.com/notedit/janus-go" "github.com/prometheus/client_golang/prometheus/promhttp" + "google.golang.org/protobuf/types/known/timestamppb" - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/client" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" - "github.com/strukturag/nextcloud-spreed-signaling/v2/session" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus" - janusapi "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) const ( @@ -89,8 +80,6 @@ const ( ) var ( - InvalidFormat = client.InvalidFormat - defaultProxyFeatures = []string{ ProxyFeatureRemoteStreams, } @@ -101,42 +90,36 @@ type ContextKey string var ( ContextKeySession = ContextKey("session") - // HelloExpected is returned if a client sends a message before the "hello" request. - HelloExpected = api.NewError("hello_expected", "Expected Hello request.") - // NoSuchSession is returned if the session to be resumed is unknown or expired. - NoSuchSession = api.NewError("no_such_session", "The session to resume does not exist.") - - TimeoutCreatingPublisher = api.NewError("timeout", "Timeout creating publisher.") - TimeoutCreatingSubscriber = api.NewError("timeout", "Timeout creating subscriber.") - TokenAuthFailed = api.NewError("auth_failed", "The token could not be authenticated.") - TokenExpired = api.NewError("token_expired", "The token is expired.") - TokenNotValidYet = api.NewError("token_not_valid_yet", "The token is not valid yet.") - UnknownClient = api.NewError("unknown_client", "Unknown client id given.") - UnsupportedCommand = api.NewError("bad_request", "Unsupported command received.") - UnsupportedMessage = api.NewError("bad_request", "Unsupported message received.") - UnsupportedPayload = api.NewError("unsupported_payload", "Unsupported payload type.") - ShutdownScheduled = api.NewError("shutdown_scheduled", "The server is scheduled to shutdown.") - RemoteSubscribersNotSupported = api.NewError("unsupported_subscriber", "Remote subscribers are not supported.") + TimeoutCreatingPublisher = signaling.NewError("timeout", "Timeout creating publisher.") + TimeoutCreatingSubscriber = signaling.NewError("timeout", "Timeout creating subscriber.") + TokenAuthFailed = signaling.NewError("auth_failed", "The token could not be authenticated.") + TokenExpired = signaling.NewError("token_expired", "The token is expired.") + TokenNotValidYet = signaling.NewError("token_not_valid_yet", "The token is not valid yet.") + UnknownClient = signaling.NewError("unknown_client", "Unknown client id given.") + UnsupportedCommand = signaling.NewError("bad_request", "Unsupported command received.") + UnsupportedMessage = signaling.NewError("bad_request", "Unsupported message received.") + UnsupportedPayload = signaling.NewError("unsupported_payload", "Unsupported payload type.") + ShutdownScheduled = signaling.NewError("shutdown_scheduled", "The server is scheduled to shutdown.") + RemoteSubscribersNotSupported = signaling.NewError("unsupported_subscriber", "Remote subscribers are not supported.") ) type ProxyServer struct { version string - country geoip.Country + country string welcomeMessage string - welcomeMsg *api.WelcomeServerMessage + welcomeMsg *signaling.WelcomeServerMessage config *goconf.ConfigFile mcuTimeout time.Duration - logger log.Logger url string - mcu sfu.SFU + mcu signaling.Mcu stopped atomic.Bool - load atomic.Uint64 + load atomic.Int64 - maxIncoming api.AtomicBandwidth - currentIncoming api.AtomicBandwidth - maxOutgoing api.AtomicBandwidth - currentOutgoing api.AtomicBandwidth + maxIncoming atomic.Int64 + currentIncoming atomic.Int64 + maxOutgoing atomic.Int64 + currentOutgoing atomic.Int64 shutdownChannel chan struct{} shutdownScheduled atomic.Bool @@ -144,37 +127,31 @@ type ProxyServer struct { upgrader websocket.Upgrader tokens ProxyTokens - statsAllowedIps atomic.Pointer[container.IPList] - trustedProxies atomic.Pointer[container.IPList] + statsAllowedIps atomic.Pointer[signaling.AllowedIps] + trustedProxies atomic.Pointer[signaling.AllowedIps] sid atomic.Uint64 - cookie *session.SessionIdCodec + cookie *signaling.SessionIdCodec + sessions map[uint64]*ProxySession sessionsLock sync.RWMutex - // +checklocks:sessionsLock - sessions map[uint64]*ProxySession + clients map[string]signaling.McuClient + clientIds map[string]string clientsLock sync.RWMutex - // +checklocks:clientsLock - clients map[string]sfu.Client - // +checklocks:clientsLock - clientIds map[string]string tokenId string tokenKey *rsa.PrivateKey - remoteTlsConfig *tls.Config // +checklocksignore: Only written to from constructor. + remoteTlsConfig *tls.Config remoteHostname string + remoteConnections map[string]*RemoteConnection remoteConnectionsLock sync.Mutex - // +checklocks:remoteConnectionsLock - remoteConnections map[string]*RemoteConnection - // +checklocks:remoteConnectionsLock - remotePublishers map[string]map[*proxyRemotePublisher]bool } -func IsPublicIP(ip net.IP) bool { - if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() { +func IsPublicIP(IP net.IP) bool { + if IP.IsLoopback() || IP.IsLinkLocalMulticast() || IP.IsLinkLocalUnicast() { return false } - if ip4 := ip.To4(); ip4 != nil { + if ip4 := IP.To4(); ip4 != nil { switch { case ip4[0] == 10: return false @@ -205,50 +182,42 @@ func GetLocalIP() (string, error) { return "", nil } -func getTargetBandwidths(logger log.Logger, config *goconf.ConfigFile) (api.Bandwidth, api.Bandwidth) { - maxIncomingValue, _ := config.GetInt("bandwidth", "incoming") - if maxIncomingValue < 0 { - maxIncomingValue = 0 +func getTargetBandwidths(config *goconf.ConfigFile) (int, int) { + maxIncoming, _ := config.GetInt("bandwidth", "incoming") + if maxIncoming < 0 { + maxIncoming = 0 } - maxIncoming := api.BandwidthFromMegabits(uint64(maxIncomingValue)) if maxIncoming > 0 { - logger.Printf("Target bandwidth for incoming streams: %s", maxIncoming) + log.Printf("Target bandwidth for incoming streams: %d MBit/s", maxIncoming) } else { - logger.Printf("Target bandwidth for incoming streams: unlimited") + log.Printf("Target bandwidth for incoming streams: unlimited") } - - maxOutgoingValue, _ := config.GetInt("bandwidth", "outgoing") - if maxOutgoingValue < 0 { - maxOutgoingValue = 0 + maxOutgoing, _ := config.GetInt("bandwidth", "outgoing") + if maxOutgoing < 0 { + maxOutgoing = 0 } - maxOutgoing := api.BandwidthFromMegabits(uint64(maxOutgoingValue)) - if maxOutgoing > 0 { - logger.Printf("Target bandwidth for outgoing streams: %s", maxOutgoing) + if maxIncoming > 0 { + log.Printf("Target bandwidth for outgoing streams: %d MBit/s", maxOutgoing) } else { - logger.Printf("Target bandwidth for outgoing streams: unlimited") + log.Printf("Target bandwidth for outgoing streams: unlimited") } return maxIncoming, maxOutgoing } -func NewProxyServer(ctx context.Context, r *mux.Router, version string, config *goconf.ConfigFile) (*ProxyServer, error) { - logger := log.LoggerFromContext(ctx) +func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile) (*ProxyServer, error) { hashKey := make([]byte, 64) if _, err := rand.Read(hashKey); err != nil { - return nil, fmt.Errorf("could not generate random hash key: %s", err) + return nil, fmt.Errorf("Could not generate random hash key: %s", err) } blockKey := make([]byte, 32) if _, err := rand.Read(blockKey); err != nil { - return nil, fmt.Errorf("could not generate random block key: %s", err) - } - - sessionIds, err := session.NewSessionIdCodec(hashKey, blockKey) - if err != nil { - return nil, fmt.Errorf("error creating session id codec: %w", err) + return nil, fmt.Errorf("Could not generate random block key: %s", err) } var tokens ProxyTokens + var err error tokenType, _ := config.GetString("app", "tokentype") if tokenType == "" { tokenType = TokenTypeDefault @@ -256,31 +225,61 @@ func NewProxyServer(ctx context.Context, r *mux.Router, version string, config * switch tokenType { case TokenTypeEtcd: - tokens, err = NewProxyTokensEtcd(logger, config) + tokens, err = NewProxyTokensEtcd(config) case TokenTypeStatic: - tokens, err = NewProxyTokensStatic(logger, config) + tokens, err = NewProxyTokensStatic(config) default: - return nil, fmt.Errorf("unsupported token type configured: %s", tokenType) + return nil, fmt.Errorf("Unsupported token type configured: %s", tokenType) } if err != nil { return nil, err } - countryString, _ := config.GetString("app", "country") - country := geoip.Country(strings.ToUpper(countryString)) - if geoip.IsValidCountry(country) { - logger.Printf("Sending %s as country information", country) - } else if country != "" { - return nil, fmt.Errorf("invalid country: %s", country) + statsAllowed, _ := config.GetString("stats", "allowed_ips") + statsAllowedIps, err := signaling.ParseAllowedIps(statsAllowed) + if err != nil { + return nil, err + } + + if !statsAllowedIps.Empty() { + log.Printf("Only allowing access to the stats endpoint from %s", statsAllowed) } else { - logger.Printf("Not sending country information") + log.Printf("No IPs configured for the stats endpoint, only allowing access from 127.0.0.1") + statsAllowedIps = signaling.DefaultAllowedIps() + } + + trustedProxies, _ := config.GetString("app", "trustedproxies") + trustedProxiesIps, err := signaling.ParseAllowedIps(trustedProxies) + if err != nil { + return nil, err + } + + if !trustedProxiesIps.Empty() { + log.Printf("Trusted proxies: %s", trustedProxiesIps) + } else { + trustedProxiesIps = signaling.DefaultTrustedProxies + log.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps) + } + + country, _ := config.GetString("app", "country") + country = strings.ToUpper(country) + if signaling.IsValidCountry(country) { + log.Printf("Sending %s as country information", country) + } else if country != "" { + return nil, fmt.Errorf("Invalid country: %s", country) + } else { + log.Printf("Not sending country information") } welcome := map[string]string{ "nextcloud-spreed-signaling-proxy": "Welcome", "version": version, } - welcomeMessage, _ := json.Marshal(welcome) + welcomeMessage, err := json.Marshal(welcome) + if err != nil { + // Should never happen. + return nil, err + } tokenId, _ := config.GetString("app", "token_id") var tokenKey *rsa.PrivateKey @@ -289,17 +288,17 @@ func NewProxyServer(ctx context.Context, r *mux.Router, version string, config * if tokenId != "" { tokenKeyFilename, _ := config.GetString("app", "token_key") if tokenKeyFilename == "" { - return nil, errors.New("no token key configured") + return nil, fmt.Errorf("No token key configured") } tokenKeyData, err := os.ReadFile(tokenKeyFilename) if err != nil { - return nil, fmt.Errorf("could not read private key from %s: %s", tokenKeyFilename, err) + return nil, fmt.Errorf("Could not read private key from %s: %s", tokenKeyFilename, err) } tokenKey, err = jwt.ParseRSAPrivateKeyFromPEM(tokenKeyData) if err != nil { - return nil, fmt.Errorf("could not parse private key from %s: %s", tokenKeyFilename, err) + return nil, fmt.Errorf("Could not parse private key from %s: %s", tokenKeyFilename, err) } - logger.Printf("Using \"%s\" as token id for remote streams", tokenId) + log.Printf("Using \"%s\" as token id for remote streams", tokenId) remoteHostname, _ = config.GetString("app", "hostname") if remoteHostname == "" { @@ -309,22 +308,24 @@ func NewProxyServer(ctx context.Context, r *mux.Router, version string, config * } } if remoteHostname == "" { - logger.Printf("WARNING: Could not determine hostname for remote streams, will be disabled. Please configure manually.") + log.Printf("WARNING: Could not determine hostname for remote streams, will be disabled. Please configure manually.") } else { - logger.Printf("Using \"%s\" as hostname for remote streams", remoteHostname) + log.Printf("Using \"%s\" as hostname for remote streams", remoteHostname) } skipverify, _ := config.GetBool("backend", "skipverify") if skipverify { - logger.Println("WARNING: Remote stream requests verification is disabled!") + log.Println("WARNING: Remote stream requests verification is disabled!") remoteTlsConfig = &tls.Config{ InsecureSkipVerify: skipverify, } } } else { - logger.Printf("No token id configured, remote streams will be disabled") + log.Printf("No token id configured, remote streams will be disabled") } + maxIncoming, maxOutgoing := getTargetBandwidths(config) + mcuTimeoutSeconds, _ := config.GetInt("mcu", "timeout") if mcuTimeoutSeconds <= 0 { mcuTimeoutSeconds = defaultMcuTimeoutSeconds @@ -335,31 +336,27 @@ func NewProxyServer(ctx context.Context, r *mux.Router, version string, config * version: version, country: country, welcomeMessage: string(welcomeMessage) + "\n", - welcomeMsg: &api.WelcomeServerMessage{ + welcomeMsg: &signaling.WelcomeServerMessage{ Version: version, Country: country, Features: defaultProxyFeatures, }, config: config, mcuTimeout: mcuTimeout, - logger: logger, shutdownChannel: make(chan struct{}), upgrader: websocket.Upgrader{ ReadBufferSize: websocketReadBufferSize, WriteBufferSize: websocketWriteBufferSize, - Subprotocols: []string{ - janus.EventsSubprotocol, - }, }, tokens: tokens, - cookie: sessionIds, + cookie: signaling.NewSessionIdCodec(hashKey, blockKey), sessions: make(map[uint64]*ProxySession), - clients: make(map[string]sfu.Client), + clients: make(map[string]signaling.McuClient), clientIds: make(map[string]string), tokenId: tokenId, @@ -367,34 +364,24 @@ func NewProxyServer(ctx context.Context, r *mux.Router, version string, config * remoteTlsConfig: remoteTlsConfig, remoteHostname: remoteHostname, remoteConnections: make(map[string]*RemoteConnection), - remotePublishers: make(map[string]map[*proxyRemotePublisher]bool), - } - - if err := result.loadConfig(config, false); err != nil { - return nil, err } + result.maxIncoming.Store(int64(maxIncoming) * 1024 * 1024) + result.maxOutgoing.Store(int64(maxOutgoing) * 1024 * 1024) + result.statsAllowedIps.Store(statsAllowedIps) + result.trustedProxies.Store(trustedProxiesIps) result.upgrader.CheckOrigin = result.checkOrigin - statsLoadCurrent.Set(0) - if debug, _ := config.GetBool("app", "debug"); debug { - logger.Println("Installing debug handlers in \"/debug/pprof\"") - s := r.PathPrefix("/debug/pprof").Subrouter() - s.HandleFunc("", result.setCommonHeaders(result.validateStatsRequest(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/debug/pprof/", http.StatusTemporaryRedirect) - }))) - s.HandleFunc("/", result.setCommonHeaders(result.validateStatsRequest(pprof.Index))) - s.HandleFunc("/cmdline", result.setCommonHeaders(result.validateStatsRequest(pprof.Cmdline))) - s.HandleFunc("/profile", result.setCommonHeaders(result.validateStatsRequest(pprof.Profile))) - s.HandleFunc("/symbol", result.setCommonHeaders(result.validateStatsRequest(pprof.Symbol))) - s.HandleFunc("/trace", result.setCommonHeaders(result.validateStatsRequest(pprof.Trace))) + log.Println("Installing debug handlers in \"/debug/pprof\"") + r.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) + r.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) + r.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) + r.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) + r.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) for _, profile := range runtimepprof.Profiles() { name := profile.Name() - handler := pprof.Handler(name) - s.HandleFunc("/"+name, result.setCommonHeaders(result.validateStatsRequest(func(w http.ResponseWriter, r *http.Request) { - handler.ServeHTTP(w, r) - }))) + r.Handle("/debug/pprof/"+name, pprof.Handler(name)) } } @@ -410,18 +397,18 @@ func (s *ProxyServer) checkOrigin(r *http.Request) bool { return true } -func (s *ProxyServer) Start(cfg *goconf.ConfigFile) error { - s.url, _ = config.GetStringOptionWithEnv(cfg, "mcu", "url") +func (s *ProxyServer) Start(config *goconf.ConfigFile) error { + s.url, _ = signaling.GetStringOptionWithEnv(config, "mcu", "url") if s.url == "" { - return errors.New("no MCU server url configured") + return fmt.Errorf("No MCU server url configured") } - mcuType, _ := cfg.GetString("mcu", "type") + mcuType, _ := config.GetString("mcu", "type") if mcuType == "" { - mcuType = sfu.TypeDefault + mcuType = signaling.McuTypeDefault } - backoff, err := async.NewExponentialBackoff(initialMcuRetry, maxMcuRetry) + backoff, err := signaling.NewExponentialBackoff(initialMcuRetry, maxMcuRetry) if err != nil { return err } @@ -429,33 +416,33 @@ func (s *ProxyServer) Start(cfg *goconf.ConfigFile) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() - var mcu sfu.SFU + var mcu signaling.Mcu for { switch mcuType { - case sfu.TypeJanus: - mcu, err = janus.NewJanusSFU(ctx, s.url, cfg) + case signaling.McuTypeJanus: + mcu, err = signaling.NewMcuJanus(ctx, s.url, config) if err == nil { - janus.RegisterStats() + signaling.RegisterJanusMcuStats() } default: - return fmt.Errorf("unsupported MCU type: %s", mcuType) + return fmt.Errorf("Unsupported MCU type: %s", mcuType) } if err == nil { mcu.SetOnConnected(s.onMcuConnected) mcu.SetOnDisconnected(s.onMcuDisconnected) err = mcu.Start(ctx) if err != nil { - s.logger.Printf("Could not create %s MCU at %s: %s", mcuType, s.url, err) + log.Printf("Could not create %s MCU at %s: %s", mcuType, s.url, err) } } if err == nil { break } - s.logger.Printf("Could not initialize %s MCU at %s (%s) will retry in %s", mcuType, s.url, err, backoff.NextWait()) + log.Printf("Could not initialize %s MCU at %s (%s) will retry in %s", mcuType, s.url, err, backoff.NextWait()) backoff.Wait(ctx) if ctx.Err() != nil { - return errors.New("cancelled") + return fmt.Errorf("Cancelled") } } @@ -486,21 +473,18 @@ loop: } } -func (s *ProxyServer) newLoadEvent(load uint64, incoming api.Bandwidth, outgoing api.Bandwidth) *proxy.ServerMessage { - msg := &proxy.ServerMessage{ +func (s *ProxyServer) newLoadEvent(load int64, incoming int64, outgoing int64) *signaling.ProxyServerMessage { + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "update-load", Load: load, }, } maxIncoming := s.maxIncoming.Load() maxOutgoing := s.maxOutgoing.Load() - if maxIncoming > 0 || maxOutgoing > 0 || incoming != 0 || outgoing != 0 { - msg.Event.Bandwidth = &proxy.EventServerBandwidth{ - Received: incoming, - Sent: outgoing, - } + if maxIncoming > 0 || maxOutgoing > 0 { + msg.Event.Bandwidth = &signaling.EventProxyServerBandwidth{} if maxIncoming > 0 { value := float64(incoming) / float64(maxIncoming) * 100 msg.Event.Bandwidth.Incoming = &value @@ -522,11 +506,10 @@ func (s *ProxyServer) updateLoad() { return } - statsLoadCurrent.Set(float64(load)) s.sendLoadToAll(load, incoming, outgoing) } -func (s *ProxyServer) sendLoadToAll(load uint64, incoming api.Bandwidth, outgoing api.Bandwidth) { +func (s *ProxyServer) sendLoadToAll(load int64, incoming int64, outgoing int64) { if s.shutdownScheduled.Load() { // Server is scheduled to shutdown, no need to update clients with current load. return @@ -562,7 +545,7 @@ func (s *ProxyServer) expireSessions() { continue } - s.logger.Printf("Delete expired session %s", session.PublicId()) + log.Printf("Delete expired session %s", session.PublicId()) s.deleteSessionLocked(session.Sid()) } } @@ -587,9 +570,9 @@ func (s *ProxyServer) ScheduleShutdown() { return } - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "shutdown-scheduled", }, } @@ -602,53 +585,41 @@ func (s *ProxyServer) ScheduleShutdown() { } } -func (s *ProxyServer) loadConfig(config *goconf.ConfigFile, fromReload bool) error { +func (s *ProxyServer) Reload(config *goconf.ConfigFile) { statsAllowed, _ := config.GetString("stats", "allowed_ips") - if statsAllowedIps, err := container.ParseIPList(statsAllowed); err == nil { + if statsAllowedIps, err := signaling.ParseAllowedIps(statsAllowed); err == nil { if !statsAllowedIps.Empty() { - s.logger.Printf("Only allowing access to the stats endpoint from %s", statsAllowed) + log.Printf("Only allowing access to the stats endpoint from %s", statsAllowed) } else { - statsAllowedIps = container.DefaultAllowedIPs() - s.logger.Printf("No IPs configured for the stats endpoint, only allowing access from %s", statsAllowedIps) + log.Printf("No IPs configured for the stats endpoint, only allowing access from 127.0.0.1") + statsAllowedIps = signaling.DefaultAllowedIps() } s.statsAllowedIps.Store(statsAllowedIps) - } else if fromReload { - s.logger.Printf("Error parsing allowed stats ips from \"%s\": %s", statsAllowedIps, err) } else { - return fmt.Errorf("error parsing allowed stats ips from \"%s\": %w", statsAllowedIps, err) + log.Printf("Error parsing allowed stats ips from \"%s\": %s", statsAllowedIps, err) } trustedProxies, _ := config.GetString("app", "trustedproxies") - if trustedProxiesIps, err := container.ParseIPList(trustedProxies); err == nil { + if trustedProxiesIps, err := signaling.ParseAllowedIps(trustedProxies); err == nil { if !trustedProxiesIps.Empty() { - s.logger.Printf("Trusted proxies: %s", trustedProxiesIps) + log.Printf("Trusted proxies: %s", trustedProxiesIps) } else { - trustedProxiesIps = client.DefaultTrustedProxies - s.logger.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps) + trustedProxiesIps = signaling.DefaultTrustedProxies + log.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps) } s.trustedProxies.Store(trustedProxiesIps) - } else if fromReload { - s.logger.Printf("Error parsing trusted proxies from \"%s\": %s", trustedProxies, err) } else { - return fmt.Errorf("error parsing trusted proxies ips from \"%s\": %w", trustedProxies, err) + log.Printf("Error parsing trusted proxies from \"%s\": %s", trustedProxies, err) } - maxIncoming, maxOutgoing := getTargetBandwidths(s.logger, config) - oldIncoming := s.maxIncoming.Swap(maxIncoming) - oldOutgoing := s.maxOutgoing.Swap(maxOutgoing) - if fromReload && (oldIncoming != maxIncoming || oldOutgoing != maxOutgoing) { + maxIncoming, maxOutgoing := getTargetBandwidths(config) + oldIncoming := s.maxIncoming.Swap(int64(maxIncoming)) + oldOutgoing := s.maxOutgoing.Swap(int64(maxOutgoing)) + if oldIncoming != int64(maxIncoming) || oldOutgoing != int64(maxOutgoing) { // Notify sessions about updated load / bandwidth usage. go s.sendLoadToAll(s.load.Load(), s.currentIncoming.Load(), s.currentOutgoing.Load()) } - return nil -} - -func (s *ProxyServer) Reload(config *goconf.ConfigFile) { - if err := s.loadConfig(config, true); err != nil { - s.logger.Printf("Error reloading configuration: %s", err) - } - s.tokens.Reload(config) s.mcu.Reload(config) } @@ -656,7 +627,6 @@ func (s *ProxyServer) Reload(config *goconf.ConfigFile) { func (s *ProxyServer) setCommonHeaders(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", "nextcloud-spreed-signaling-proxy/"+s.version) - w.Header().Set("X-Spreed-Signaling-Features", strings.Join(s.welcomeMsg.Features, ", ")) f(w, r) } } @@ -668,26 +638,19 @@ func (s *ProxyServer) welcomeHandler(w http.ResponseWriter, r *http.Request) { } func (s *ProxyServer) proxyHandler(w http.ResponseWriter, r *http.Request) { - addr := client.GetRealUserIP(r, s.trustedProxies.Load()) + addr := signaling.GetRealUserIP(r, s.trustedProxies.Load()) header := http.Header{} header.Set("Server", "nextcloud-spreed-signaling-proxy/"+s.version) header.Set("X-Spreed-Signaling-Features", strings.Join(s.welcomeMsg.Features, ", ")) conn, err := s.upgrader.Upgrade(w, r, header) if err != nil { - s.logger.Printf("Could not upgrade request from %s: %s", addr, err) + log.Printf("Could not upgrade request from %s: %s", addr, err) return } - agent := r.Header.Get("User-Agent") - ctx := log.NewLoggerContext(r.Context(), s.logger) - if conn.Subprotocol() == janus.EventsSubprotocol { - janus.RunEventsHandler(ctx, s.mcu, conn, addr, agent) - return - } - - client, err := NewProxyClient(ctx, s, conn, addr, agent) + client, err := NewProxyClient(r.Context(), s, conn, addr) if err != nil { - s.logger.Printf("Could not create client for %s: %s", addr, err) + log.Printf("Could not create client for %s: %s", addr, err) return } @@ -695,15 +658,15 @@ func (s *ProxyServer) proxyHandler(w http.ResponseWriter, r *http.Request) { client.ReadPump() } -func (s *ProxyServer) clientClosed(client *ProxyClient) { - s.logger.Printf("Connection from %s closed", client.RemoteAddr()) +func (s *ProxyServer) clientClosed(client *signaling.Client) { + log.Printf("Connection from %s closed", client.RemoteAddr()) } func (s *ProxyServer) onMcuConnected() { - s.logger.Printf("Connection to %s established", s.url) - msg := &proxy.ServerMessage{ + log.Printf("Connection to %s established", s.url) + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "backend-connected", }, } @@ -719,10 +682,10 @@ func (s *ProxyServer) onMcuDisconnected() { return } - s.logger.Printf("Connection to %s lost", s.url) - msg := &proxy.ServerMessage{ + log.Printf("Connection to %s lost", s.url) + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "backend-disconnected", }, } @@ -739,9 +702,9 @@ func (s *ProxyServer) sendCurrentLoad(session *ProxySession) { } func (s *ProxyServer) sendShutdownScheduled(session *ProxySession) { - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "shutdown-scheduled", }, } @@ -750,48 +713,48 @@ func (s *ProxyServer) sendShutdownScheduled(session *ProxySession) { func (s *ProxyServer) processMessage(client *ProxyClient, data []byte) { if proxyDebugMessages { - s.logger.Printf("Message: %s", string(data)) + log.Printf("Message: %s", string(data)) } - var message proxy.ClientMessage + var message signaling.ProxyClientMessage if err := message.UnmarshalJSON(data); err != nil { if session := client.GetSession(); session != nil { - s.logger.Printf("Error decoding message from client %s: %v", session.PublicId(), err) + log.Printf("Error decoding message from client %s: %v", session.PublicId(), err) } else { - s.logger.Printf("Error decoding message from %s: %v", client.RemoteAddr(), err) + log.Printf("Error decoding message from %s: %v", client.RemoteAddr(), err) } - client.SendError(InvalidFormat) + client.SendError(signaling.InvalidFormat) return } if err := message.CheckValid(); err != nil { if session := client.GetSession(); session != nil { - s.logger.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) + log.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) } else { - s.logger.Printf("Invalid message %+v from %s: %v", message, client.RemoteAddr(), err) + log.Printf("Invalid message %+v from %s: %v", message, client.RemoteAddr(), err) } - client.SendMessage(message.NewErrorServerMessage(InvalidFormat)) + client.SendMessage(message.NewErrorServerMessage(signaling.InvalidFormat)) return } session := client.GetSession() if session == nil { if message.Type != "hello" { - client.SendMessage(message.NewErrorServerMessage(HelloExpected)) + client.SendMessage(message.NewErrorServerMessage(signaling.HelloExpected)) return } var session *ProxySession - if resumeId := api.PublicSessionId(message.Hello.ResumeId); resumeId != "" { + if resumeId := message.Hello.ResumeId; resumeId != "" { if data, err := s.cookie.DecodePublic(resumeId); err == nil { session = s.GetSession(data.Sid) } if session == nil || resumeId != session.PublicId() { - client.SendMessage(message.NewErrorServerMessage(NoSuchSession)) + client.SendMessage(message.NewErrorServerMessage(signaling.NoSuchSession)) return } - s.logger.Printf("Resumed session %s", session.PublicId()) + log.Printf("Resumed session %s", session.PublicId()) session.MarkUsed() if s.shutdownScheduled.Load() { s.sendShutdownScheduled(session) @@ -802,7 +765,7 @@ func (s *ProxyServer) processMessage(client *ProxyClient, data []byte) { } else { var err error if session, err = s.NewSession(message.Hello); err != nil { - if e, ok := err.(*api.Error); ok { + if e, ok := err.(*signaling.Error); ok { client.SendMessage(message.NewErrorServerMessage(e)) } else { client.SendMessage(message.NewWrappedErrorServerMessage(err)) @@ -813,19 +776,19 @@ func (s *ProxyServer) processMessage(client *ProxyClient, data []byte) { prev := session.SetClient(client) if prev != nil { - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "bye", - Bye: &proxy.ByeServerMessage{ + Bye: &signaling.ByeProxyServerMessage{ Reason: "session_resumed", }, } prev.SendMessage(msg) } - response := &proxy.ServerMessage{ + response := &signaling.ProxyServerMessage{ Id: message.Id, Type: "hello", - Hello: &proxy.HelloServerMessage{ - Version: api.HelloVersionV1, + Hello: &signaling.HelloProxyServerMessage{ + Version: signaling.HelloVersionV1, SessionId: session.PublicId(), Server: s.welcomeMsg, }, @@ -856,7 +819,7 @@ func (s *ProxyServer) processMessage(client *ProxyClient, data []byte) { type emptyInitiator struct{} -func (i *emptyInitiator) Country() geoip.Country { +func (i *emptyInitiator) Country() string { return "" } @@ -864,24 +827,24 @@ type proxyRemotePublisher struct { proxy *ProxyServer remoteUrl string - publisherId api.PublicSessionId + publisherId string } -func (p *proxyRemotePublisher) PublisherId() api.PublicSessionId { +func (p *proxyRemotePublisher) PublisherId() string { return p.publisherId } -func (p *proxyRemotePublisher) StartPublishing(ctx context.Context, publisher sfu.RemotePublisherProperties) error { +func (p *proxyRemotePublisher) StartPublishing(ctx context.Context, publisher signaling.McuRemotePublisherProperties) error { conn, err := p.proxy.getRemoteConnection(p.remoteUrl) if err != nil { return err } - if _, err := conn.RequestMessage(ctx, &proxy.ClientMessage{ + if _, err := conn.RequestMessage(ctx, &signaling.ProxyClientMessage{ Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "publish-remote", - ClientId: string(p.publisherId), + ClientId: p.publisherId, Hostname: p.proxy.remoteHostname, Port: publisher.Port(), RtcpPort: publisher.RtcpPort(), @@ -893,19 +856,17 @@ func (p *proxyRemotePublisher) StartPublishing(ctx context.Context, publisher sf return nil } -func (p *proxyRemotePublisher) StopPublishing(ctx context.Context, publisher sfu.RemotePublisherProperties) error { - defer p.proxy.removeRemotePublisher(p) - +func (p *proxyRemotePublisher) StopPublishing(ctx context.Context, publisher signaling.McuRemotePublisherProperties) error { conn, err := p.proxy.getRemoteConnection(p.remoteUrl) if err != nil { return err } - if _, err := conn.RequestMessage(ctx, &proxy.ClientMessage{ + if _, err := conn.RequestMessage(ctx, &signaling.ProxyClientMessage{ Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "unpublish-remote", - ClientId: string(p.publisherId), + ClientId: p.publisherId, Hostname: p.proxy.remoteHostname, Port: publisher.Port(), RtcpPort: publisher.RtcpPort(), @@ -917,17 +878,17 @@ func (p *proxyRemotePublisher) StopPublishing(ctx context.Context, publisher sfu return nil } -func (p *proxyRemotePublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, error) { +func (p *proxyRemotePublisher) GetStreams(ctx context.Context) ([]signaling.PublisherStream, error) { conn, err := p.proxy.getRemoteConnection(p.remoteUrl) if err != nil { return nil, err } - response, err := conn.RequestMessage(ctx, &proxy.ClientMessage{ + response, err := conn.RequestMessage(ctx, &signaling.ProxyClientMessage{ Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "get-publisher-streams", - ClientId: string(p.publisherId), + ClientId: p.publisherId, }, }) if err != nil { @@ -937,54 +898,7 @@ func (p *proxyRemotePublisher) GetStreams(ctx context.Context) ([]sfu.PublisherS return response.Command.Streams, nil } -func (s *ProxyServer) addRemotePublisher(publisher *proxyRemotePublisher) { - s.remoteConnectionsLock.Lock() - defer s.remoteConnectionsLock.Unlock() - - publishers, found := s.remotePublishers[publisher.remoteUrl] - if !found { - publishers = make(map[*proxyRemotePublisher]bool) - s.remotePublishers[publisher.remoteUrl] = publishers - } - - publishers[publisher] = true - s.logger.Printf("Add remote publisher to %s", publisher.remoteUrl) -} - -func (s *ProxyServer) hasRemotePublishers() bool { - s.remoteConnectionsLock.Lock() - defer s.remoteConnectionsLock.Unlock() - - return len(s.remotePublishers) > 0 -} - -func (s *ProxyServer) removeRemotePublisher(publisher *proxyRemotePublisher) { - s.remoteConnectionsLock.Lock() - defer s.remoteConnectionsLock.Unlock() - - s.logger.Printf("Removing remote publisher to %s", publisher.remoteUrl) - publishers, found := s.remotePublishers[publisher.remoteUrl] - if !found { - return - } - - delete(publishers, publisher) - if len(publishers) > 0 { - return - } - - delete(s.remotePublishers, publisher.remoteUrl) - if conn, found := s.remoteConnections[publisher.remoteUrl]; found { - delete(s.remoteConnections, publisher.remoteUrl) - if err := conn.Close(); err != nil { - s.logger.Printf("Error closing remote connection to %s: %s", publisher.remoteUrl, err) - } else { - s.logger.Printf("Remote connection to %s closed", publisher.remoteUrl) - } - } -} - -func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, session *ProxySession, message *proxy.ClientMessage) { +func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, session *ProxySession, message *signaling.ProxyClientMessage) { cmd := message.Command statsCommandMessagesTotal.WithLabelValues(cmd.Type).Inc() @@ -1002,32 +916,32 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s id := uuid.New().String() settings := cmd.PublisherSettings if settings == nil { - settings = &sfu.NewPublisherSettings{ + settings = &signaling.NewPublisherSettings{ Bitrate: cmd.Bitrate, // nolint MediaTypes: cmd.MediaTypes, // nolint } } - publisher, err := s.mcu.NewPublisher(ctx2, session, api.PublicSessionId(id), cmd.Sid, cmd.StreamType, *settings, &emptyInitiator{}) + publisher, err := s.mcu.NewPublisher(ctx2, session, id, cmd.Sid, cmd.StreamType, *settings, &emptyInitiator{}) if err == context.DeadlineExceeded { - s.logger.Printf("Timeout while creating %s publisher %s for %s", cmd.StreamType, id, session.PublicId()) + log.Printf("Timeout while creating %s publisher %s for %s", cmd.StreamType, id, session.PublicId()) session.sendMessage(message.NewErrorServerMessage(TimeoutCreatingPublisher)) return } else if err != nil { - s.logger.Printf("Error while creating %s publisher %s for %s: %s", cmd.StreamType, id, session.PublicId(), err) + log.Printf("Error while creating %s publisher %s for %s: %s", cmd.StreamType, id, session.PublicId(), err) session.sendMessage(message.NewWrappedErrorServerMessage(err)) return } - s.logger.Printf("Created %s publisher %s as %s for %s", cmd.StreamType, publisher.Id(), id, session.PublicId()) + log.Printf("Created %s publisher %s as %s for %s", cmd.StreamType, publisher.Id(), id, session.PublicId()) session.StorePublisher(ctx, id, publisher) s.StoreClient(id, publisher) - response := &proxy.ServerMessage{ + response := &signaling.ProxyServerMessage{ Id: message.Id, Type: "command", - Command: &proxy.CommandServerMessage{ + Command: &signaling.CommandProxyServerMessage{ Id: id, - Bitrate: publisher.MaxBitrate(), + Bitrate: int(publisher.MaxBitrate()), }, } session.sendMessage(response) @@ -1036,20 +950,20 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s case "create-subscriber": id := uuid.New().String() publisherId := cmd.PublisherId - var subscriber sfu.Subscriber + var subscriber signaling.McuSubscriber var err error handleCreateError := func(err error) { if err == context.DeadlineExceeded { - s.logger.Printf("Timeout while creating %s subscriber on %s for %s", cmd.StreamType, publisherId, session.PublicId()) + log.Printf("Timeout while creating %s subscriber on %s for %s", cmd.StreamType, publisherId, session.PublicId()) session.sendMessage(message.NewErrorServerMessage(TimeoutCreatingSubscriber)) return - } else if errors.Is(err, janus.ErrRemoteStreamsNotSupported) { + } else if errors.Is(err, signaling.ErrRemoteStreamsNotSupported) { session.sendMessage(message.NewErrorServerMessage(RemoteSubscribersNotSupported)) return } - s.logger.Printf("Error while creating %s subscriber on %s for %s: %s", cmd.StreamType, publisherId, session.PublicId(), err) + log.Printf("Error while creating %s subscriber on %s for %s: %s", cmd.StreamType, publisherId, session.PublicId(), err) session.sendMessage(message.NewWrappedErrorServerMessage(err)) } @@ -1059,7 +973,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s return } - remoteMcu, ok := s.mcu.(sfu.RemoteSfu) + remoteMcu, ok := s.mcu.(signaling.RemoteMcu) if !ok { session.sendMessage(message.NewErrorServerMessage(RemoteSubscribersNotSupported)) return @@ -1067,7 +981,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s claims, _, err := s.parseToken(cmd.RemoteToken) if err != nil { - if e, ok := err.(*api.Error); ok { + if e, ok := err.(*signaling.Error); ok { client.SendMessage(message.NewErrorServerMessage(e)) } else { client.SendMessage(message.NewWrappedErrorServerMessage(err)) @@ -1075,7 +989,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s return } - if claims.Subject != string(publisherId) { + if claims.Subject != publisherId { session.sendMessage(message.NewErrorServerMessage(TokenAuthFailed)) return } @@ -1083,7 +997,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s subCtx, cancel := context.WithTimeout(ctx, remotePublisherTimeout) defer cancel() - s.logger.Printf("Creating remote subscriber for %s on %s", publisherId, cmd.RemoteUrl) + log.Printf("Creating remote subscriber for %s on %s", publisherId, cmd.RemoteUrl) controller := &proxyRemotePublisher{ proxy: s, @@ -1091,7 +1005,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s publisherId: publisherId, } - var publisher sfu.RemotePublisher + var publisher signaling.McuRemotePublisher publisher, err = remoteMcu.NewRemotePublisher(subCtx, session, controller, cmd.StreamType) if err != nil { handleCreateError(err) @@ -1102,15 +1016,13 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s go publisher.Close(context.Background()) }() - s.addRemotePublisher(controller) - subscriber, err = remoteMcu.NewRemoteSubscriber(subCtx, session, publisher) if err != nil { handleCreateError(err) return } - s.logger.Printf("Created remote %s subscriber %s as %s for %s on %s", cmd.StreamType, subscriber.Id(), id, session.PublicId(), cmd.RemoteUrl) + log.Printf("Created remote %s subscriber %s as %s for %s on %s", cmd.StreamType, subscriber.Id(), id, session.PublicId(), cmd.RemoteUrl) } else { ctx2, cancel := context.WithTimeout(ctx, s.mcuTimeout) defer cancel() @@ -1121,16 +1033,16 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s return } - s.logger.Printf("Created %s subscriber %s as %s for %s", cmd.StreamType, subscriber.Id(), id, session.PublicId()) + log.Printf("Created %s subscriber %s as %s for %s", cmd.StreamType, subscriber.Id(), id, session.PublicId()) } session.StoreSubscriber(ctx, id, subscriber) s.StoreClient(id, subscriber) - response := &proxy.ServerMessage{ + response := &signaling.ProxyServerMessage{ Id: message.Id, Type: "command", - Command: &proxy.CommandServerMessage{ + Command: &signaling.CommandProxyServerMessage{ Id: id, Sid: subscriber.Sid(), }, @@ -1145,7 +1057,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s return } - publisher, ok := client.(sfu.Publisher) + publisher, ok := client.(signaling.McuPublisher) if !ok { session.sendMessage(message.NewErrorServerMessage(UnknownClient)) return @@ -1161,14 +1073,14 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s } go func() { - s.logger.Printf("Closing %s publisher %s as %s", client.StreamType(), client.Id(), cmd.ClientId) + log.Printf("Closing %s publisher %s as %s", client.StreamType(), client.Id(), cmd.ClientId) client.Close(context.Background()) }() - response := &proxy.ServerMessage{ + response := &signaling.ProxyServerMessage{ Id: message.Id, Type: "command", - Command: &proxy.CommandServerMessage{ + Command: &signaling.CommandProxyServerMessage{ Id: cmd.ClientId, }, } @@ -1180,7 +1092,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s return } - subscriber, ok := client.(sfu.Subscriber) + subscriber, ok := client.(signaling.McuSubscriber) if !ok { session.sendMessage(message.NewErrorServerMessage(UnknownClient)) return @@ -1196,14 +1108,14 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s } go func() { - s.logger.Printf("Closing %s subscriber %s as %s", client.StreamType(), client.Id(), cmd.ClientId) + log.Printf("Closing %s subscriber %s as %s", client.StreamType(), client.Id(), cmd.ClientId) client.Close(context.Background()) }() - response := &proxy.ServerMessage{ + response := &signaling.ProxyServerMessage{ Id: message.Id, Type: "command", - Command: &proxy.CommandServerMessage{ + Command: &signaling.CommandProxyServerMessage{ Id: cmd.ClientId, }, } @@ -1215,7 +1127,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s return } - publisher, ok := client.(sfu.RemoteAwarePublisher) + publisher, ok := client.(signaling.McuPublisher) if !ok { session.sendMessage(message.NewErrorServerMessage(UnknownClient)) return @@ -1225,8 +1137,9 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s defer cancel() if err := publisher.PublishRemote(ctx2, session.PublicId(), cmd.Hostname, cmd.Port, cmd.RtcpPort); err != nil { - if je, ok := internal.AsErrorType[*janusapi.ErrorMsg](err); !ok || je.Err.Code != janusapi.JANUS_VIDEOROOM_ERROR_ID_EXISTS { - s.logger.Printf("Error publishing %s %s to remote %s (port=%d, rtcpPort=%d): %s", publisher.StreamType(), cmd.ClientId, cmd.Hostname, cmd.Port, cmd.RtcpPort, err) + var je *janus.ErrorMsg + if !errors.As(err, &je) || je.Err.Code != signaling.JANUS_VIDEOROOM_ERROR_ID_EXISTS { + log.Printf("Error publishing %s %s to remote %s (port=%d, rtcpPort=%d): %s", publisher.StreamType(), cmd.ClientId, cmd.Hostname, cmd.Port, cmd.RtcpPort, err) session.sendMessage(message.NewWrappedErrorServerMessage(err)) return } @@ -1235,7 +1148,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s defer cancel() if err := publisher.UnpublishRemote(ctx2, session.PublicId(), cmd.Hostname, cmd.Port, cmd.RtcpPort); err != nil { - s.logger.Printf("Error unpublishing old %s %s to remote %s (port=%d, rtcpPort=%d): %s", publisher.StreamType(), cmd.ClientId, cmd.Hostname, cmd.Port, cmd.RtcpPort, err) + log.Printf("Error unpublishing old %s %s to remote %s (port=%d, rtcpPort=%d): %s", publisher.StreamType(), cmd.ClientId, cmd.Hostname, cmd.Port, cmd.RtcpPort, err) session.sendMessage(message.NewWrappedErrorServerMessage(err)) return } @@ -1244,17 +1157,17 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s defer cancel() if err := publisher.PublishRemote(ctx2, session.PublicId(), cmd.Hostname, cmd.Port, cmd.RtcpPort); err != nil { - s.logger.Printf("Error publishing %s %s to remote %s (port=%d, rtcpPort=%d): %s", publisher.StreamType(), cmd.ClientId, cmd.Hostname, cmd.Port, cmd.RtcpPort, err) + log.Printf("Error publishing %s %s to remote %s (port=%d, rtcpPort=%d): %s", publisher.StreamType(), cmd.ClientId, cmd.Hostname, cmd.Port, cmd.RtcpPort, err) session.sendMessage(message.NewWrappedErrorServerMessage(err)) return } } session.AddRemotePublisher(publisher, cmd.Hostname, cmd.Port, cmd.RtcpPort) - response := &proxy.ServerMessage{ + response := &signaling.ProxyServerMessage{ Id: message.Id, Type: "command", - Command: &proxy.CommandServerMessage{ + Command: &signaling.CommandProxyServerMessage{ Id: cmd.ClientId, }, } @@ -1266,7 +1179,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s return } - publisher, ok := client.(sfu.RemoteAwarePublisher) + publisher, ok := client.(signaling.McuPublisher) if !ok { session.sendMessage(message.NewErrorServerMessage(UnknownClient)) return @@ -1276,17 +1189,17 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s defer cancel() if err := publisher.UnpublishRemote(ctx2, session.PublicId(), cmd.Hostname, cmd.Port, cmd.RtcpPort); err != nil { - s.logger.Printf("Error unpublishing %s %s from remote %s: %s", publisher.StreamType(), cmd.ClientId, cmd.Hostname, err) + log.Printf("Error unpublishing %s %s from remote %s: %s", publisher.StreamType(), cmd.ClientId, cmd.Hostname, err) session.sendMessage(message.NewWrappedErrorServerMessage(err)) return } session.RemoveRemotePublisher(publisher, cmd.Hostname, cmd.Port, cmd.RtcpPort) - response := &proxy.ServerMessage{ + response := &signaling.ProxyServerMessage{ Id: message.Id, Type: "command", - Command: &proxy.CommandServerMessage{ + Command: &signaling.CommandProxyServerMessage{ Id: cmd.ClientId, }, } @@ -1298,7 +1211,7 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s return } - publisher, ok := client.(sfu.PublisherWithStreams) + publisher, ok := client.(signaling.McuPublisher) if !ok { session.sendMessage(message.NewErrorServerMessage(UnknownClient)) return @@ -1306,27 +1219,27 @@ func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, s streams, err := publisher.GetStreams(ctx) if err != nil { - s.logger.Printf("Could not get streams of publisher %s: %s", publisher.Id(), err) + log.Printf("Could not get streams of publisher %s: %s", publisher.Id(), err) session.sendMessage(message.NewWrappedErrorServerMessage(err)) return } - response := &proxy.ServerMessage{ + response := &signaling.ProxyServerMessage{ Id: message.Id, Type: "command", - Command: &proxy.CommandServerMessage{ + Command: &signaling.CommandProxyServerMessage{ Id: cmd.ClientId, Streams: streams, }, } session.sendMessage(response) default: - s.logger.Printf("Unsupported command %+v", message.Command) + log.Printf("Unsupported command %+v", message.Command) session.sendMessage(message.NewErrorServerMessage(UnsupportedCommand)) } } -func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, session *ProxySession, message *proxy.ClientMessage) { +func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, session *ProxySession, message *signaling.ProxyClientMessage) { payload := message.Payload mcuClient := s.GetClient(payload.ClientId) if mcuClient == nil { @@ -1336,7 +1249,7 @@ func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, s statsPayloadMessagesTotal.WithLabelValues(payload.Type).Inc() - var mcuData *api.MessageClientMessageData + var mcuData *signaling.MessageClientMessageData switch payload.Type { case "offer": fallthrough @@ -1345,7 +1258,7 @@ func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, s case "selectStream": fallthrough case "candidate": - mcuData = &api.MessageClientMessageData{ + mcuData = &signaling.MessageClientMessageData{ RoomType: string(mcuClient.StreamType()), Type: payload.Type, Sid: payload.Sid, @@ -1353,10 +1266,10 @@ func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, s } case "endOfCandidates": // Ignore but confirm, not passed along to Janus anyway. - session.sendMessage(&proxy.ServerMessage{ + session.sendMessage(&signaling.ProxyServerMessage{ Id: message.Id, Type: "payload", - Payload: &proxy.PayloadServerMessage{ + Payload: &signaling.PayloadProxyServerMessage{ Type: payload.Type, ClientId: payload.ClientId, }, @@ -1365,7 +1278,7 @@ func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, s case "requestoffer": fallthrough case "sendoffer": - mcuData = &api.MessageClientMessageData{ + mcuData = &signaling.MessageClientMessageData{ RoomType: string(mcuClient.StreamType()), Type: payload.Type, Sid: payload.Sid, @@ -1376,7 +1289,7 @@ func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, s } if err := mcuData.CheckValid(); err != nil { - s.logger.Printf("Received invalid payload %+v for %s client %s: %s", mcuData, mcuClient.StreamType(), payload.ClientId, err) + log.Printf("Received invalid payload %+v for %s client %s: %s", mcuData, mcuClient.StreamType(), payload.ClientId, err) session.sendMessage(message.NewErrorServerMessage(UnsupportedPayload)) return } @@ -1384,21 +1297,16 @@ func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, s ctx2, cancel := context.WithTimeout(ctx, s.mcuTimeout) defer cancel() - mcuClient.SendMessage(ctx2, nil, mcuData, func(err error, response api.StringMap) { - var responseMsg *proxy.ServerMessage - if errors.Is(err, api.ErrCandidateFiltered) { - // Silently ignore filtered candidates. - err = nil - } - + mcuClient.SendMessage(ctx2, nil, mcuData, func(err error, response map[string]interface{}) { + var responseMsg *signaling.ProxyServerMessage if err != nil { - s.logger.Printf("Error sending %+v to %s client %s: %s", mcuData, mcuClient.StreamType(), payload.ClientId, err) + log.Printf("Error sending %+v to %s client %s: %s", mcuData, mcuClient.StreamType(), payload.ClientId, err) responseMsg = message.NewWrappedErrorServerMessage(err) } else { - responseMsg = &proxy.ServerMessage{ + responseMsg = &signaling.ProxyServerMessage{ Id: message.Id, Type: "payload", - Payload: &proxy.PayloadServerMessage{ + Payload: &signaling.PayloadProxyServerMessage{ Type: payload.Type, ClientId: payload.ClientId, Payload: response, @@ -1410,39 +1318,39 @@ func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, s }) } -func (s *ProxyServer) processBye(ctx context.Context, client *ProxyClient, session *ProxySession, message *proxy.ClientMessage) { - s.logger.Printf("Closing session %s", session.PublicId()) +func (s *ProxyServer) processBye(ctx context.Context, client *ProxyClient, session *ProxySession, message *signaling.ProxyClientMessage) { + log.Printf("Closing session %s", session.PublicId()) s.DeleteSession(session.Sid()) } -func (s *ProxyServer) parseToken(tokenValue string) (*proxy.TokenClaims, string, error) { +func (s *ProxyServer) parseToken(tokenValue string) (*signaling.TokenClaims, string, error) { reason := "auth-failed" - token, err := jwt.ParseWithClaims(tokenValue, &proxy.TokenClaims{}, func(token *jwt.Token) (any, error) { + token, err := jwt.ParseWithClaims(tokenValue, &signaling.TokenClaims{}, func(token *jwt.Token) (interface{}, error) { // Don't forget to validate the alg is what you expect: if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { - s.logger.Printf("Unexpected signing method: %v", token.Header["alg"]) + log.Printf("Unexpected signing method: %v", token.Header["alg"]) reason = "unsupported-signing-method" - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } - claims, ok := token.Claims.(*proxy.TokenClaims) + claims, ok := token.Claims.(*signaling.TokenClaims) if !ok { - s.logger.Printf("Unsupported claims type: %+v", token.Claims) + log.Printf("Unsupported claims type: %+v", token.Claims) reason = "unsupported-claims" - return nil, errors.New("unsupported claims type") + return nil, fmt.Errorf("Unsupported claims type") } tokenKey, err := s.tokens.Get(claims.Issuer) if err != nil { - s.logger.Printf("Could not get token for %s: %s", claims.Issuer, err) + log.Printf("Could not get token for %s: %s", claims.Issuer, err) reason = "missing-issuer" return nil, err } if tokenKey == nil || tokenKey.key == nil { - s.logger.Printf("Issuer %s is not supported", claims.Issuer) + log.Printf("Issuer %s is not supported", claims.Issuer) reason = "unsupported-issuer" - return nil, errors.New("no key found for issuer") + return nil, fmt.Errorf("No key found for issuer") } return tokenKey.key, nil @@ -1461,7 +1369,7 @@ func (s *ProxyServer) parseToken(tokenValue string) (*proxy.TokenClaims, string, return nil, reason, TokenAuthFailed } - claims, ok := token.Claims.(*proxy.TokenClaims) + claims, ok := token.Claims.(*signaling.TokenClaims) if !ok || !token.Valid { return nil, "auth-failed", TokenAuthFailed } @@ -1475,9 +1383,9 @@ func (s *ProxyServer) parseToken(tokenValue string) (*proxy.TokenClaims, string, return claims, "", nil } -func (s *ProxyServer) NewSession(hello *proxy.HelloClientMessage) (*ProxySession, error) { +func (s *ProxyServer) NewSession(hello *signaling.HelloProxyClientMessage) (*ProxySession, error) { if proxyDebugMessages { - s.logger.Printf("Hello: %+v", hello) + log.Printf("Hello: %+v", hello) } claims, reason, err := s.parseToken(hello.Token) @@ -1491,9 +1399,9 @@ func (s *ProxyServer) NewSession(hello *proxy.HelloClientMessage) (*ProxySession sid = s.sid.Add(1) } - sessionIdData := &session.SessionIdData{ + sessionIdData := &signaling.SessionIdData{ Sid: sid, - Created: time.Now().UnixMicro(), + Created: timestamppb.Now(), } encoded, err := s.cookie.EncodePublic(sessionIdData) @@ -1501,7 +1409,7 @@ func (s *ProxyServer) NewSession(hello *proxy.HelloClientMessage) (*ProxySession return nil, err } - s.logger.Printf("Created session %s for %+v", encoded, claims) + log.Printf("Created session %s for %+v", encoded, claims) session := NewProxySession(s, sid, encoded) s.StoreSession(sid, session) statsSessionsCurrent.Inc() @@ -1542,7 +1450,6 @@ func (s *ProxyServer) DeleteSession(id uint64) { s.deleteSessionLocked(id) } -// +checklocks:s.sessionsLock func (s *ProxyServer) deleteSessionLocked(id uint64) { if session, found := s.sessions[id]; found { delete(s.sessions, id) @@ -1553,14 +1460,14 @@ func (s *ProxyServer) deleteSessionLocked(id uint64) { } } -func (s *ProxyServer) StoreClient(id string, client sfu.Client) { +func (s *ProxyServer) StoreClient(id string, client signaling.McuClient) { s.clientsLock.Lock() defer s.clientsLock.Unlock() s.clients[id] = client s.clientIds[client.Id()] = id } -func (s *ProxyServer) DeleteClient(id string, client sfu.Client) bool { +func (s *ProxyServer) DeleteClient(id string, client signaling.McuClient) bool { s.clientsLock.Lock() defer s.clientsLock.Unlock() if _, found := s.clients[id]; !found { @@ -1582,43 +1489,34 @@ func (s *ProxyServer) HasClients() bool { return len(s.clients) > 0 } -func (s *ProxyServer) GetClientsLoad() (load uint64, incoming api.Bandwidth, outgoing api.Bandwidth) { +func (s *ProxyServer) GetClientsLoad() (load int64, incoming int64, outgoing int64) { s.clientsLock.RLock() defer s.clientsLock.RUnlock() for _, c := range s.clients { - // Use "current" bandwidth usage if supported. - if bw, ok := c.(sfu.ClientWithBandwidth); ok { - if bandwidth := bw.Bandwidth(); bandwidth != nil { - incoming += bandwidth.Received - outgoing += bandwidth.Sent - continue - } - } - - bitrate := c.MaxBitrate() - if _, ok := c.(sfu.Publisher); ok { + bitrate := int64(c.MaxBitrate()) + load += bitrate + if _, ok := c.(signaling.McuPublisher); ok { incoming += bitrate - } else if _, ok := c.(sfu.Subscriber); ok { + } else if _, ok := c.(signaling.McuSubscriber); ok { outgoing += bitrate } } - load = incoming.Bits() + outgoing.Bits() - load = min(uint64(len(s.clients)), load/1024) + load = load / 1024 return } -func (s *ProxyServer) GetClient(id string) sfu.Client { +func (s *ProxyServer) GetClient(id string) signaling.McuClient { s.clientsLock.RLock() defer s.clientsLock.RUnlock() return s.clients[id] } -func (s *ProxyServer) GetPublisher(publisherId string) sfu.Publisher { +func (s *ProxyServer) GetPublisher(publisherId string) signaling.McuPublisher { s.clientsLock.RLock() defer s.clientsLock.RUnlock() for _, c := range s.clients { - pub, ok := c.(sfu.Publisher) + pub, ok := c.(signaling.McuPublisher) if !ok { continue } @@ -1630,14 +1528,14 @@ func (s *ProxyServer) GetPublisher(publisherId string) sfu.Publisher { return nil } -func (s *ProxyServer) GetClientId(client sfu.Client) string { +func (s *ProxyServer) GetClientId(client signaling.McuClient) string { s.clientsLock.RLock() defer s.clientsLock.RUnlock() return s.clientIds[client.Id()] } -func (s *ProxyServer) getStats() api.StringMap { - result := api.StringMap{ +func (s *ProxyServer) getStats() map[string]interface{} { + result := map[string]interface{}{ "sessions": s.GetSessionsCount(), "load": s.load.Load(), "mcu": s.mcu.GetStats(), @@ -1646,14 +1544,14 @@ func (s *ProxyServer) getStats() api.StringMap { } func (s *ProxyServer) allowStatsAccess(r *http.Request) bool { - addr := client.GetRealUserIP(r, s.trustedProxies.Load()) + addr := signaling.GetRealUserIP(r, s.trustedProxies.Load()) ip := net.ParseIP(addr) if len(ip) == 0 { return false } allowed := s.statsAllowedIps.Load() - return allowed != nil && allowed.Contains(ip) + return allowed != nil && allowed.Allowed(ip) } func (s *ProxyServer) validateStatsRequest(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { @@ -1671,7 +1569,7 @@ func (s *ProxyServer) statsHandler(w http.ResponseWriter, r *http.Request) { stats := s.getStats() statsData, err := json.MarshalIndent(stats, "", " ") if err != nil { - s.logger.Printf("Could not serialize stats %+v: %s", stats, err) + log.Printf("Could not serialize stats %+v: %s", stats, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } @@ -1696,7 +1594,7 @@ func (s *ProxyServer) getRemoteConnection(url string) (*RemoteConnection, error) return conn, nil } - conn, err := NewRemoteConnection(s, url, s.tokenId, s.tokenKey, s.remoteTlsConfig) + conn, err := NewRemoteConnection(url, s.tokenId, s.tokenKey, s.remoteTlsConfig) if err != nil { return nil, err } @@ -1705,7 +1603,7 @@ func (s *ProxyServer) getRemoteConnection(url string) (*RemoteConnection, error) return conn, nil } -func (s *ProxyServer) PublisherDeleted(publisher sfu.Publisher) { +func (s *ProxyServer) PublisherDeleted(publisher signaling.McuPublisher) { s.sessionsLock.RLock() defer s.sessionsLock.RUnlock() @@ -1713,12 +1611,3 @@ func (s *ProxyServer) PublisherDeleted(publisher sfu.Publisher) { session.OnPublisherDeleted(publisher) } } - -func (s *ProxyServer) RemotePublisherDeleted(publisherId api.PublicSessionId) { - s.sessionsLock.RLock() - defer s.sessionsLock.RUnlock() - - for _, session := range s.sessions { - session.OnRemotePublisherDeleted(publisherId) - } -} diff --git a/cmd/proxy/proxy_server_test.go b/proxy/proxy_server_test.go similarity index 50% rename from cmd/proxy/proxy_server_test.go rename to proxy/proxy_server_test.go index 305e9f3..973b6dc 100644 --- a/cmd/proxy/proxy_server_test.go +++ b/proxy/proxy_server_test.go @@ -36,7 +36,6 @@ import ( "sync" "sync/atomic" "testing" - "testing/synctest" "time" "github.com/dlintw/goconf" @@ -45,15 +44,7 @@ import ( "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) const ( @@ -64,10 +55,10 @@ const ( ) func getWebsocketUrl(url string) string { - if url, found := strings.CutPrefix(url, "http://"); found { - return "ws://" + url + "/proxy" - } else if url, found := strings.CutPrefix(url, "https://"); found { - return "wss://" + url + "/proxy" + if strings.HasPrefix(url, "http://") { + return "ws://" + url[7:] + "/proxy" + } else if strings.HasPrefix(url, "https://") { + return "wss://" + url[8:] + "/proxy" } else { panic("Unsupported URL: " + url) } @@ -97,7 +88,7 @@ func WaitForProxyServer(ctx context.Context, t *testing.T, proxy *ProxyServer) { case <-ctx.Done(): proxy.clientsLock.Lock() proxy.remoteConnectionsLock.Lock() - assert.Fail(t, "Error waiting for proxy to terminate", "clients %+v / sessions %+v / remoteConnections %+v: %+v", clients, sessions, remoteConnections, ctx.Err()) + assert.Fail(t, fmt.Sprintf("Error waiting for clients %+v / sessions %+v / remoteConnections %+v to terminate: %+v", proxy.clients, proxy.sessions, proxy.remoteConnections, ctx.Err())) proxy.remoteConnectionsLock.Unlock() proxy.clientsLock.Unlock() return @@ -146,9 +137,7 @@ func newProxyServerForTest(t *testing.T) (*ProxyServer, *rsa.PrivateKey, *httpte config := goconf.NewConfigFile() config.AddOption("tokens", TokenIdForTest, pubkey.Name()) - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - proxy, err = NewProxyServer(ctx, r, "0.0", config) + proxy, err = NewProxyServer(r, "0.0", config) require.NoError(err) server := httptest.NewServer(r) @@ -160,10 +149,10 @@ func newProxyServerForTest(t *testing.T) (*ProxyServer, *rsa.PrivateKey, *httpte } func TestTokenValid(t *testing.T) { - t.Parallel() - proxyServer, key, _ := newProxyServerForTest(t) + signaling.CatchLogForTest(t) + proxy, key, _ := newProxyServerForTest(t) - claims := &proxy.TokenClaims{ + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge / 2)), Issuer: TokenIdForTest, @@ -173,20 +162,20 @@ func TestTokenValid(t *testing.T) { tokenString, err := token.SignedString(key) require.NoError(t, err) - hello := &proxy.HelloClientMessage{ + hello := &signaling.HelloProxyClientMessage{ Version: "1.0", Token: tokenString, } - if session, err := proxyServer.NewSession(hello); assert.NoError(t, err) { - defer proxyServer.DeleteSession(session.Sid()) + if session, err := proxy.NewSession(hello); assert.NoError(t, err) { + defer proxy.DeleteSession(session.Sid()) } } func TestTokenNotSigned(t *testing.T) { - t.Parallel() - proxyServer, _, _ := newProxyServerForTest(t) + signaling.CatchLogForTest(t) + proxy, _, _ := newProxyServerForTest(t) - claims := &proxy.TokenClaims{ + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge / 2)), Issuer: TokenIdForTest, @@ -196,22 +185,22 @@ func TestTokenNotSigned(t *testing.T) { tokenString, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType) require.NoError(t, err) - hello := &proxy.HelloClientMessage{ + hello := &signaling.HelloProxyClientMessage{ Version: "1.0", Token: tokenString, } - if session, err := proxyServer.NewSession(hello); !assert.ErrorIs(t, err, TokenAuthFailed) { + if session, err := proxy.NewSession(hello); !assert.ErrorIs(t, err, TokenAuthFailed) { if session != nil { - defer proxyServer.DeleteSession(session.Sid()) + defer proxy.DeleteSession(session.Sid()) } } } func TestTokenUnknown(t *testing.T) { - t.Parallel() - proxyServer, key, _ := newProxyServerForTest(t) + signaling.CatchLogForTest(t) + proxy, key, _ := newProxyServerForTest(t) - claims := &proxy.TokenClaims{ + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge / 2)), Issuer: TokenIdForTest + "2", @@ -221,22 +210,22 @@ func TestTokenUnknown(t *testing.T) { tokenString, err := token.SignedString(key) require.NoError(t, err) - hello := &proxy.HelloClientMessage{ + hello := &signaling.HelloProxyClientMessage{ Version: "1.0", Token: tokenString, } - if session, err := proxyServer.NewSession(hello); !assert.ErrorIs(t, err, TokenAuthFailed) { + if session, err := proxy.NewSession(hello); !assert.ErrorIs(t, err, TokenAuthFailed) { if session != nil { - defer proxyServer.DeleteSession(session.Sid()) + defer proxy.DeleteSession(session.Sid()) } } } func TestTokenInFuture(t *testing.T) { - t.Parallel() - proxyServer, key, _ := newProxyServerForTest(t) + signaling.CatchLogForTest(t) + proxy, key, _ := newProxyServerForTest(t) - claims := &proxy.TokenClaims{ + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), Issuer: TokenIdForTest, @@ -246,22 +235,22 @@ func TestTokenInFuture(t *testing.T) { tokenString, err := token.SignedString(key) require.NoError(t, err) - hello := &proxy.HelloClientMessage{ + hello := &signaling.HelloProxyClientMessage{ Version: "1.0", Token: tokenString, } - if session, err := proxyServer.NewSession(hello); !assert.ErrorIs(t, err, TokenNotValidYet) { + if session, err := proxy.NewSession(hello); !assert.ErrorIs(t, err, TokenNotValidYet) { if session != nil { - defer proxyServer.DeleteSession(session.Sid()) + defer proxy.DeleteSession(session.Sid()) } } } func TestTokenExpired(t *testing.T) { - t.Parallel() - proxyServer, key, _ := newProxyServerForTest(t) + signaling.CatchLogForTest(t) + proxy, key, _ := newProxyServerForTest(t) - claims := &proxy.TokenClaims{ + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge * 2)), Issuer: TokenIdForTest, @@ -271,19 +260,18 @@ func TestTokenExpired(t *testing.T) { tokenString, err := token.SignedString(key) require.NoError(t, err) - hello := &proxy.HelloClientMessage{ + hello := &signaling.HelloProxyClientMessage{ Version: "1.0", Token: tokenString, } - if session, err := proxyServer.NewSession(hello); !assert.ErrorIs(t, err, TokenExpired) { + if session, err := proxy.NewSession(hello); !assert.ErrorIs(t, err, TokenExpired) { if session != nil { - defer proxyServer.DeleteSession(session.Sid()) + defer proxy.DeleteSession(session.Sid()) } } } func TestPublicIPs(t *testing.T) { - t.Parallel() assert := assert.New(t) public := []string{ "8.8.8.8", @@ -316,7 +304,7 @@ func TestPublicIPs(t *testing.T) { } func TestWebsocketFeatures(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) _, _, server := newProxyServerForTest(t) @@ -325,26 +313,29 @@ func TestWebsocketFeatures(t *testing.T) { defer conn.Close() // nolint if server := response.Header.Get("Server"); !strings.HasPrefix(server, "nextcloud-spreed-signaling-proxy/") { - assert.Fail("expected valid server header", "received \"%s\"", server) + assert.Fail("expected valid server header, got \"%s\"", server) } features := response.Header.Get("X-Spreed-Signaling-Features") featuresList := make(map[string]bool) - for f := range internal.SplitEntries(features, ",") { - if _, found := featuresList[f]; found { - assert.Fail("duplicate feature", "id \"%s\" in \"%s\"", f, features) + for _, f := range strings.Split(features, ",") { + f = strings.TrimSpace(f) + if f != "" { + if _, found := featuresList[f]; found { + assert.Fail("duplicate feature id \"%s\" in \"%s\"", f, features) + } + featuresList[f] = true } - featuresList[f] = true } assert.NotEmpty(featuresList, "expected valid features header, got \"%s\"", features) if _, found := featuresList["remote-streams"]; !found { - assert.Fail("expected feature \"remote-streams\"", "received \"%s\"", features) + assert.Fail("expected feature \"remote-streams\", got \"%s\"", features) } assert.NoError(conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Time{})) } func TestProxyCreateSession(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) require := require.New(t) _, key, server := newProxyServerForTest(t) @@ -369,10 +360,6 @@ type TestMCU struct { t *testing.T } -func (m *TestMCU) GetBandwidthLimits() (api.Bandwidth, api.Bandwidth) { - return 0, 0 -} - func (m *TestMCU) Start(ctx context.Context) error { return nil } @@ -389,33 +376,25 @@ func (m *TestMCU) SetOnConnected(f func()) { func (m *TestMCU) SetOnDisconnected(f func()) { } -func (m *TestMCU) GetStats() any { +func (m *TestMCU) GetStats() interface{} { return nil } -func (m *TestMCU) GetServerInfoSfu() *talk.BackendServerInfoSfu { - return nil -} - -func (m *TestMCU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { +func (m *TestMCU) NewPublisher(ctx context.Context, listener signaling.McuListener, id string, sid string, streamType signaling.StreamType, settings signaling.NewPublisherSettings, initiator signaling.McuInitiator) (signaling.McuPublisher, error) { return nil, errors.New("not implemented") } -func (m *TestMCU) NewSubscriber(ctx context.Context, listener sfu.Listener, publisher api.PublicSessionId, streamType sfu.StreamType, initiator sfu.Initiator) (sfu.Subscriber, error) { +func (m *TestMCU) NewSubscriber(ctx context.Context, listener signaling.McuListener, publisher string, streamType signaling.StreamType, initiator signaling.McuInitiator) (signaling.McuSubscriber, error) { return nil, errors.New("not implemented") } type TestMCUPublisher struct { - id api.PublicSessionId + id string sid string - streamType sfu.StreamType + streamType signaling.StreamType } func (p *TestMCUPublisher) Id() string { - return string(p.id) -} - -func (p *TestMCUPublisher) PublisherId() api.PublicSessionId { return p.id } @@ -423,231 +402,40 @@ func (p *TestMCUPublisher) Sid() string { return p.sid } -func (p *TestMCUPublisher) StreamType() sfu.StreamType { +func (p *TestMCUPublisher) StreamType() signaling.StreamType { return p.streamType } -func (p *TestMCUPublisher) MaxBitrate() api.Bandwidth { +func (p *TestMCUPublisher) MaxBitrate() int { return 0 } func (p *TestMCUPublisher) Close(ctx context.Context) { } -func (p *TestMCUPublisher) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { +func (p *TestMCUPublisher) SendMessage(ctx context.Context, message *signaling.MessageClientMessage, data *signaling.MessageClientMessageData, callback func(error, map[string]interface{})) { callback(errors.New("not implemented"), nil) } -func (p *TestMCUPublisher) HasMedia(sfu.MediaType) bool { +func (p *TestMCUPublisher) HasMedia(signaling.MediaType) bool { return false } -func (p *TestMCUPublisher) SetMedia(mediaTypes sfu.MediaType) { +func (p *TestMCUPublisher) SetMedia(mediaTypes signaling.MediaType) { } -func (p *TestMCUPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, error) { +func (p *TestMCUPublisher) GetStreams(ctx context.Context) ([]signaling.PublisherStream, error) { return nil, errors.New("not implemented") } -func (p *TestMCUPublisher) PublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error { +func (p *TestMCUPublisher) PublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { return errors.New("not implemented") } -func (p *TestMCUPublisher) UnpublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error { +func (p *TestMCUPublisher) UnpublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { return errors.New("not implemented") } -type PublisherTestMCU struct { - TestMCU -} - -type TestPublisherWithBandwidth struct { - TestMCUPublisher - - t *testing.T - bandwidth *sfu.ClientBandwidthInfo -} - -func (p *TestPublisherWithBandwidth) Bandwidth() *sfu.ClientBandwidthInfo { - return p.bandwidth -} - -func (p *TestPublisherWithBandwidth) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { - switch data.Type { - case "offer": - assert.Equal(p.t, mock.MockSdpOfferAudioAndVideo, data.Payload["sdp"]) - assert.NotNil(p.t, data.OfferSdp) - callback(nil, api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioAndVideo, - }) - case "requestoffer": - callback(nil, api.StringMap{ - "type": "offer", - "sdp": mock.MockSdpOfferAudioOnly, - }) - default: - callback(fmt.Errorf("type %s not implemented", data.Type), nil) - } -} - -func (m *PublisherTestMCU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { - publisher := &TestPublisherWithBandwidth{ - TestMCUPublisher: TestMCUPublisher{ - id: id, - sid: sid, - streamType: streamType, - }, - - t: m.t, - bandwidth: &sfu.ClientBandwidthInfo{ - Sent: api.BandwidthFromBytes(1000), - Received: api.BandwidthFromBytes(2000), - }, - } - return publisher, nil -} - -func NewPublisherTestMCU(t *testing.T) *PublisherTestMCU { - return &PublisherTestMCU{ - TestMCU: TestMCU{ - t: t, - }, - } -} - -func TestProxyPublisherBandwidth(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) - t.Cleanup(func() { - assert.EqualValues(0, proxyServer.GetSessionsCount()) - }) - - proxyServer.maxIncoming.Store(api.BandwidthFromBytes(10000)) - proxyServer.maxOutgoing.Store(api.BandwidthFromBytes(10000)) - - mcu := NewPublisherTestMCU(t) - proxyServer.mcu = mcu - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - assert.EqualValues(0, proxyServer.GetSessionsCount()) - - client := NewProxyTestClient(ctx, t, server.URL) - defer client.CloseWithBye() - - require.NoError(client.SendHello(key)) - - if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello) - } - - _, err := client.RunUntilLoad(ctx, 0) - assert.NoError(err) - - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "2345", - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "create-publisher", - StreamType: sfu.StreamTypeVideo, - }, - })) - - var clientId string - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("2345", message.Id) - if err := checkMessageType(message, "command"); assert.NoError(err) { - assert.NotEmpty(message.Command.Id) - clientId = message.Command.Id - } - } - require.NotEmpty(clientId) - - if publisher := proxyServer.GetPublisher(clientId); assert.NotNil(publisher) { - assert.Equal(clientId, proxyServer.GetClientId(publisher)) - } - - proxyServer.updateLoad() - - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - if err := checkMessageType(message, "event"); assert.NoError(err) && assert.Equal("update-load", message.Event.Type) { - assert.EqualValues(1, message.Event.Load) - if bw := message.Event.Bandwidth; assert.NotNil(bw) { - if assert.NotNil(bw.Incoming) { - assert.InEpsilon(20, *bw.Incoming, 0.0001) - } - if assert.NotNil(bw.Outgoing) { - assert.InEpsilon(10, *bw.Outgoing, 0.0001) - } - } - } - } - - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "3456", - Type: "payload", - Payload: &proxy.PayloadClientMessage{ - Type: "offer", - ClientId: clientId, - Payload: api.StringMap{ - "type": "offer", - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - }, - })) - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("3456", message.Id) - assert.Equal("payload", message.Type) - if payload := message.Payload; assert.NotNil(payload) { - assert.Equal(clientId, payload.ClientId) - assert.Equal("offer", payload.Type) - assert.Equal("answer", payload.Payload["type"]) - assert.Equal(mock.MockSdpAnswerAudioAndVideo, payload.Payload["sdp"]) - } - } - - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "4567", - Type: "payload", - Payload: &proxy.PayloadClientMessage{ - Type: "endOfCandidates", - ClientId: clientId, - }, - })) - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("4567", message.Id) - assert.Equal("payload", message.Type) - if payload := message.Payload; assert.NotNil(payload) { - assert.Equal(clientId, payload.ClientId) - assert.Equal("endOfCandidates", payload.Type) - assert.Empty(payload.Payload) - } - } - - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "5678", - Type: "payload", - Payload: &proxy.PayloadClientMessage{ - Type: "requestoffer", - ClientId: clientId, - }, - })) - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("5678", message.Id) - assert.Equal("payload", message.Type) - if payload := message.Payload; assert.NotNil(payload) { - assert.Equal(clientId, payload.ClientId) - assert.Equal("requestoffer", payload.Type) - assert.Equal("offer", payload.Payload["type"]) - assert.Equal(mock.MockSdpOfferAudioOnly, payload.Payload["sdp"]) - } - } -} - type HangingTestMCU struct { TestMCU ctx context.Context @@ -672,7 +460,7 @@ func NewHangingTestMCU(t *testing.T) *HangingTestMCU { } } -func (m *HangingTestMCU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { +func (m *HangingTestMCU) NewPublisher(ctx context.Context, listener signaling.McuListener, id string, sid string, streamType signaling.StreamType, settings signaling.NewPublisherSettings, initiator signaling.McuInitiator) (signaling.McuPublisher, error) { ctx2, cancel := context.WithTimeout(m.ctx, testTimeout*2) defer cancel() @@ -690,7 +478,7 @@ func (m *HangingTestMCU) NewPublisher(ctx context.Context, listener sfu.Listener } } -func (m *HangingTestMCU) NewSubscriber(ctx context.Context, listener sfu.Listener, publisher api.PublicSessionId, streamType sfu.StreamType, initiator sfu.Initiator) (sfu.Subscriber, error) { +func (m *HangingTestMCU) NewSubscriber(ctx context.Context, listener signaling.McuListener, publisher string, streamType signaling.StreamType, initiator signaling.McuInitiator) (signaling.McuSubscriber, error) { ctx2, cancel := context.WithTimeout(m.ctx, testTimeout*2) defer cancel() @@ -709,13 +497,13 @@ func (m *HangingTestMCU) NewSubscriber(ctx context.Context, listener sfu.Listene } func TestProxyCancelOnClose(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) + proxy, key, server := newProxyServerForTest(t) mcu := NewHangingTestMCU(t) - proxyServer.mcu = mcu + proxy.mcu = mcu ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -732,19 +520,19 @@ func TestProxyCancelOnClose(t *testing.T) { _, err := client.RunUntilLoad(ctx, 0) assert.NoError(err) - require.NoError(client.WriteJSON(&proxy.ClientMessage{ + require.NoError(client.WriteJSON(&signaling.ProxyClientMessage{ Id: "2345", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "create-publisher", - StreamType: sfu.StreamTypeVideo, + StreamType: signaling.StreamTypeVideo, }, })) // Simulate expired session while request is still being processed. go func() { <-mcu.creating - if session := proxyServer.GetSession(1); assert.NotNil(session) { + if session := proxy.GetSession(1); assert.NotNil(session) { session.Close() } }() @@ -777,7 +565,7 @@ func NewCodecsTestMCU(t *testing.T) *CodecsTestMCU { } } -func (m *CodecsTestMCU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { +func (m *CodecsTestMCU) NewPublisher(ctx context.Context, listener signaling.McuListener, id string, sid string, streamType signaling.StreamType, settings signaling.NewPublisherSettings, initiator signaling.McuInitiator) (signaling.McuPublisher, error) { assert.Equal(m.t, "opus,g722", settings.AudioCodec) assert.Equal(m.t, "vp9,vp8,av1", settings.VideoCodec) return &TestMCUPublisher{ @@ -788,13 +576,13 @@ func (m *CodecsTestMCU) NewPublisher(ctx context.Context, listener sfu.Listener, } func TestProxyCodecs(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) + proxy, key, server := newProxyServerForTest(t) mcu := NewCodecsTestMCU(t) - proxyServer.mcu = mcu + proxy.mcu = mcu ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -811,13 +599,13 @@ func TestProxyCodecs(t *testing.T) { _, err := client.RunUntilLoad(ctx, 0) assert.NoError(err) - require.NoError(client.WriteJSON(&proxy.ClientMessage{ + require.NoError(client.WriteJSON(&signaling.ProxyClientMessage{ Id: "2345", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "create-publisher", - StreamType: sfu.StreamTypeVideo, - PublisherSettings: &sfu.NewPublisherSettings{ + StreamType: signaling.StreamTypeVideo, + PublisherSettings: &signaling.NewPublisherSettings{ AudioCodec: "opus,g722", VideoCodec: "vp9,vp8,av1", }, @@ -832,121 +620,6 @@ func TestProxyCodecs(t *testing.T) { } } -type StreamTestMCU struct { - TestMCU - - streams []sfu.PublisherStream -} - -type StreamsTestPublisher struct { - TestMCUPublisher - - streams []sfu.PublisherStream -} - -func (m *StreamTestMCU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { - return &StreamsTestPublisher{ - TestMCUPublisher: TestMCUPublisher{ - id: id, - sid: sid, - streamType: streamType, - }, - - streams: m.streams, - }, nil -} - -func (p *StreamsTestPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, error) { - return p.streams, nil -} - -func NewStreamTestMCU(t *testing.T, streams []sfu.PublisherStream) *StreamTestMCU { - return &StreamTestMCU{ - TestMCU: TestMCU{ - t: t, - }, - - streams: streams, - } -} - -func TestProxyStreams(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) - - streams := []sfu.PublisherStream{ - { - Mid: "0", - Mindex: 0, - Type: "audio", - Codec: "opus", - }, - { - Mid: "1", - Mindex: 1, - Type: "video", - Codec: "vp8", - }, - } - - mcu := NewStreamTestMCU(t, streams) - proxyServer.mcu = mcu - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client := NewProxyTestClient(ctx, t, server.URL) - defer client.CloseWithBye() - - require.NoError(client.SendHello(key)) - - if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello) - } - - _, err := client.RunUntilLoad(ctx, 0) - assert.NoError(err) - - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "2345", - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "create-publisher", - StreamType: sfu.StreamTypeVideo, - }, - })) - - var clientId string - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("2345", message.Id) - if err := checkMessageType(message, "command"); assert.NoError(err) { - if assert.NotEmpty(message.Command.Id) { - clientId = message.Command.Id - } - } - } - - require.NotEmpty(clientId, "should have received publisher id") - - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "3456", - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "get-publisher-streams", - ClientId: clientId, - }, - })) - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("3456", message.Id) - if err := checkMessageType(message, "command"); assert.NoError(err) { - assert.Equal(clientId, message.Command.Id) - assert.Equal(streams, message.Command.Streams) - } - } -} - type RemoteSubscriberTestMCU struct { TestMCU @@ -965,48 +638,35 @@ func NewRemoteSubscriberTestMCU(t *testing.T) *RemoteSubscriberTestMCU { type TestRemotePublisher struct { t *testing.T - streamType sfu.StreamType + streamType signaling.StreamType refcnt atomic.Int32 closed context.Context closeFunc context.CancelFunc - listener sfu.Listener - controller sfu.RemotePublisherController } func (p *TestRemotePublisher) Id() string { return "id" } -func (p *TestRemotePublisher) PublisherId() api.PublicSessionId { - return "id" -} - func (p *TestRemotePublisher) Sid() string { return "sid" } -func (p *TestRemotePublisher) StreamType() sfu.StreamType { +func (p *TestRemotePublisher) StreamType() signaling.StreamType { return p.streamType } -func (p *TestRemotePublisher) MaxBitrate() api.Bandwidth { +func (p *TestRemotePublisher) MaxBitrate() int { return 0 } func (p *TestRemotePublisher) Close(ctx context.Context) { - if count := p.refcnt.Add(-1); assert.GreaterOrEqual(p.t, int(count), 0) && count == 0 { + if count := p.refcnt.Add(-1); assert.True(p.t, count >= 0) && count == 0 { p.closeFunc() - shortCtx, cancel := context.WithTimeout(ctx, time.Millisecond) - defer cancel() - // Won't be able to preform remote call to actually stop publishing. - if err := p.controller.StopPublishing(shortCtx, p); !errors.Is(err, context.DeadlineExceeded) { - assert.NoError(p.t, err) - } - p.listener.PublisherClosed(p) } } -func (p *TestRemotePublisher) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { +func (p *TestRemotePublisher) SendMessage(ctx context.Context, message *signaling.MessageClientMessage, data *signaling.MessageClientMessageData, callback func(error, map[string]interface{})) { callback(errors.New("not implemented"), nil) } @@ -1018,14 +678,7 @@ func (p *TestRemotePublisher) RtcpPort() int { return 2 } -func (p *TestRemotePublisher) SetMedia(mediaType sfu.MediaType) { -} - -func (p *TestRemotePublisher) HasMedia(mediaType sfu.MediaType) bool { - return false -} - -func (m *RemoteSubscriberTestMCU) NewRemotePublisher(ctx context.Context, listener sfu.Listener, controller sfu.RemotePublisherController, streamType sfu.StreamType) (sfu.RemotePublisher, error) { +func (m *RemoteSubscriberTestMCU) NewRemotePublisher(ctx context.Context, listener signaling.McuListener, controller signaling.RemotePublisherController, streamType signaling.StreamType) (signaling.McuRemotePublisher, error) { require.Nil(m.t, m.publisher) assert.EqualValues(m.t, "video", streamType) closeCtx, closeFunc := context.WithCancel(context.Background()) @@ -1035,8 +688,6 @@ func (m *RemoteSubscriberTestMCU) NewRemotePublisher(ctx context.Context, listen streamType: streamType, closed: closeCtx, closeFunc: closeFunc, - listener: listener, - controller: controller, } m.publisher.refcnt.Add(1) return m.publisher, nil @@ -1058,11 +709,11 @@ func (s *TestRemoteSubscriber) Sid() string { return "sid" } -func (s *TestRemoteSubscriber) StreamType() sfu.StreamType { +func (s *TestRemoteSubscriber) StreamType() signaling.StreamType { return s.publisher.StreamType() } -func (s *TestRemoteSubscriber) MaxBitrate() api.Bandwidth { +func (s *TestRemoteSubscriber) MaxBitrate() int { return 0 } @@ -1071,15 +722,15 @@ func (s *TestRemoteSubscriber) Close(ctx context.Context) { s.closeFunc() } -func (s *TestRemoteSubscriber) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { +func (s *TestRemoteSubscriber) SendMessage(ctx context.Context, message *signaling.MessageClientMessage, data *signaling.MessageClientMessageData, callback func(error, map[string]interface{})) { callback(errors.New("not implemented"), nil) } -func (s *TestRemoteSubscriber) Publisher() api.PublicSessionId { - return api.PublicSessionId(s.publisher.Id()) +func (s *TestRemoteSubscriber) Publisher() string { + return s.publisher.Id() } -func (m *RemoteSubscriberTestMCU) NewRemoteSubscriber(ctx context.Context, listener sfu.Listener, publisher sfu.RemotePublisher) (sfu.RemoteSubscriber, error) { +func (m *RemoteSubscriberTestMCU) NewRemoteSubscriber(ctx context.Context, listener signaling.McuListener, publisher signaling.McuRemotePublisher) (signaling.McuRemoteSubscriber, error) { require.Nil(m.t, m.subscriber) pub, ok := publisher.(*TestRemotePublisher) require.True(m.t, ok) @@ -1096,17 +747,17 @@ func (m *RemoteSubscriberTestMCU) NewRemoteSubscriber(ctx context.Context, liste } func TestProxyRemoteSubscriber(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) + proxy, key, server := newProxyServerForTest(t) mcu := NewRemoteSubscriberTestMCU(t) - proxyServer.mcu = mcu + proxy.mcu = mcu // Unused but must be set so remote subscribing works - proxyServer.tokenId = "token" - proxyServer.tokenKey = key - proxyServer.remoteHostname = "test-hostname" + proxy.tokenId = "token" + proxy.tokenKey = key + proxy.remoteHostname = "test-hostname" ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -1123,24 +774,24 @@ func TestProxyRemoteSubscriber(t *testing.T) { _, err := client.RunUntilLoad(ctx, 0) assert.NoError(err) - publisherId := api.PublicSessionId("the-publisher-id") - claims := &proxy.TokenClaims{ + publisherId := "the-publisher-id" + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge / 2)), Issuer: TokenIdForTest, - Subject: string(publisherId), + Subject: publisherId, }, } token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) tokenString, err := token.SignedString(key) require.NoError(err) - require.NoError(client.WriteJSON(&proxy.ClientMessage{ + require.NoError(client.WriteJSON(&signaling.ProxyClientMessage{ Id: "2345", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "create-subscriber", - StreamType: sfu.StreamTypeVideo, + StreamType: signaling.StreamTypeVideo, PublisherId: publisherId, RemoteUrl: "https://remote-hostname", RemoteToken: tokenString, @@ -1156,12 +807,10 @@ func TestProxyRemoteSubscriber(t *testing.T) { } } - assert.True(proxyServer.hasRemotePublishers()) - - require.NoError(client.WriteJSON(&proxy.ClientMessage{ + require.NoError(client.WriteJSON(&signaling.ProxyClientMessage{ Id: "3456", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "delete-subscriber", ClientId: clientId, }, @@ -1186,22 +835,20 @@ func TestProxyRemoteSubscriber(t *testing.T) { assert.Fail("publisher was not closed") } } - - assert.False(proxyServer.hasRemotePublishers()) } func TestProxyCloseRemoteOnSessionClose(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) + proxy, key, server := newProxyServerForTest(t) mcu := NewRemoteSubscriberTestMCU(t) - proxyServer.mcu = mcu + proxy.mcu = mcu // Unused but must be set so remote subscribing works - proxyServer.tokenId = "token" - proxyServer.tokenKey = key - proxyServer.remoteHostname = "test-hostname" + proxy.tokenId = "token" + proxy.tokenKey = key + proxy.remoteHostname = "test-hostname" ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -1218,24 +865,24 @@ func TestProxyCloseRemoteOnSessionClose(t *testing.T) { _, err := client.RunUntilLoad(ctx, 0) assert.NoError(err) - publisherId := api.PublicSessionId("the-publisher-id") - claims := &proxy.TokenClaims{ + publisherId := "the-publisher-id" + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge / 2)), Issuer: TokenIdForTest, - Subject: string(publisherId), + Subject: publisherId, }, } token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) tokenString, err := token.SignedString(key) require.NoError(err) - require.NoError(client.WriteJSON(&proxy.ClientMessage{ + require.NoError(client.WriteJSON(&signaling.ProxyClientMessage{ Id: "2345", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "create-subscriber", - StreamType: sfu.StreamTypeVideo, + StreamType: signaling.StreamTypeVideo, PublisherId: publisherId, RemoteUrl: "https://remote-hostname", RemoteToken: tokenString, @@ -1283,16 +930,14 @@ func NewUnpublishRemoteTestMCU(t *testing.T) *UnpublishRemoteTestMCU { type UnpublishRemoteTestPublisher struct { TestMCUPublisher - t *testing.T // +checklocksignore: Only written to from constructor. + t *testing.T - mu sync.RWMutex - // +checklocks:mu - remoteId api.PublicSessionId - // +checklocks:mu + mu sync.RWMutex + remoteId string remoteData *remotePublisherData } -func (m *UnpublishRemoteTestMCU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { +func (m *UnpublishRemoteTestMCU) NewPublisher(ctx context.Context, listener signaling.McuListener, id string, sid string, streamType signaling.StreamType, settings signaling.NewPublisherSettings, initiator signaling.McuInitiator) (signaling.McuPublisher, error) { publisher := &UnpublishRemoteTestPublisher{ TestMCUPublisher: TestMCUPublisher{ id: id, @@ -1306,7 +951,7 @@ func (m *UnpublishRemoteTestMCU) NewPublisher(ctx context.Context, listener sfu. return publisher, nil } -func (p *UnpublishRemoteTestPublisher) getRemoteId() api.PublicSessionId { +func (p *UnpublishRemoteTestPublisher) getRemoteId() string { p.mu.RLock() defer p.mu.RUnlock() return p.remoteId @@ -1325,7 +970,7 @@ func (p *UnpublishRemoteTestPublisher) clearRemote() { p.remoteData = nil } -func (p *UnpublishRemoteTestPublisher) PublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error { +func (p *UnpublishRemoteTestPublisher) PublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { p.mu.Lock() defer p.mu.Unlock() if assert.Empty(p.t, p.remoteId) { @@ -1339,14 +984,14 @@ func (p *UnpublishRemoteTestPublisher) PublishRemote(ctx context.Context, remote return nil } -func (p *UnpublishRemoteTestPublisher) UnpublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error { +func (p *UnpublishRemoteTestPublisher) UnpublishRemote(ctx context.Context, remoteId string, hostname string, port int, rtcpPort int) error { p.mu.Lock() defer p.mu.Unlock() assert.Equal(p.t, remoteId, p.remoteId) if remoteData := p.remoteData; assert.NotNil(p.t, remoteData) && assert.Equal(p.t, remoteData.hostname, hostname) && - assert.Equal(p.t, remoteData.port, port) && - assert.Equal(p.t, remoteData.rtcpPort, rtcpPort) { + assert.EqualValues(p.t, remoteData.port, port) && + assert.EqualValues(p.t, remoteData.rtcpPort, rtcpPort) { p.remoteId = "" p.remoteData = nil } @@ -1354,13 +999,13 @@ func (p *UnpublishRemoteTestPublisher) UnpublishRemote(ctx context.Context, remo } func TestProxyUnpublishRemote(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) + proxy, key, server := newProxyServerForTest(t) mcu := NewUnpublishRemoteTestMCU(t) - proxyServer.mcu = mcu + proxy.mcu = mcu ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -1377,18 +1022,18 @@ func TestProxyUnpublishRemote(t *testing.T) { _, err := client1.RunUntilLoad(ctx, 0) assert.NoError(err) - publisherId := api.PublicSessionId("the-publisher-id") - require.NoError(client1.WriteJSON(&proxy.ClientMessage{ + publisherId := "the-publisher-id" + require.NoError(client1.WriteJSON(&signaling.ProxyClientMessage{ Id: "2345", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "create-publisher", PublisherId: publisherId, Sid: "1234-abcd", - StreamType: sfu.StreamTypeVideo, - PublisherSettings: &sfu.NewPublisherSettings{ + StreamType: signaling.StreamTypeVideo, + PublisherSettings: &signaling.NewPublisherSettings{ Bitrate: 1234567, - MediaTypes: sfu.MediaTypeAudio | sfu.MediaTypeVideo, + MediaTypes: signaling.MediaTypeAudio | signaling.MediaTypeVideo, }, }, })) @@ -1415,12 +1060,12 @@ func TestProxyUnpublishRemote(t *testing.T) { _, err = client2.RunUntilLoad(ctx, 0) assert.NoError(err) - require.NoError(client2.WriteJSON(&proxy.ClientMessage{ + require.NoError(client2.WriteJSON(&signaling.ProxyClientMessage{ Id: "3456", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "publish-remote", - StreamType: sfu.StreamTypeVideo, + StreamType: signaling.StreamTypeVideo, ClientId: clientId, Hostname: "remote-host", Port: 10001, @@ -1439,17 +1084,17 @@ func TestProxyUnpublishRemote(t *testing.T) { assert.Equal(hello2.Hello.SessionId, publisher.getRemoteId()) if remoteData := publisher.getRemoteData(); assert.NotNil(remoteData) { assert.Equal("remote-host", remoteData.hostname) - assert.Equal(10001, remoteData.port) - assert.Equal(10002, remoteData.rtcpPort) + assert.EqualValues(10001, remoteData.port) + assert.EqualValues(10002, remoteData.rtcpPort) } } - require.NoError(client2.WriteJSON(&proxy.ClientMessage{ + require.NoError(client2.WriteJSON(&signaling.ProxyClientMessage{ Id: "4567", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "unpublish-remote", - StreamType: sfu.StreamTypeVideo, + StreamType: signaling.StreamTypeVideo, ClientId: clientId, Hostname: "remote-host", Port: 10001, @@ -1471,13 +1116,13 @@ func TestProxyUnpublishRemote(t *testing.T) { } func TestProxyUnpublishRemotePublisherClosed(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) + proxy, key, server := newProxyServerForTest(t) mcu := NewUnpublishRemoteTestMCU(t) - proxyServer.mcu = mcu + proxy.mcu = mcu ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -1494,18 +1139,18 @@ func TestProxyUnpublishRemotePublisherClosed(t *testing.T) { _, err := client1.RunUntilLoad(ctx, 0) assert.NoError(err) - publisherId := api.PublicSessionId("the-publisher-id") - require.NoError(client1.WriteJSON(&proxy.ClientMessage{ + publisherId := "the-publisher-id" + require.NoError(client1.WriteJSON(&signaling.ProxyClientMessage{ Id: "2345", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "create-publisher", PublisherId: publisherId, Sid: "1234-abcd", - StreamType: sfu.StreamTypeVideo, - PublisherSettings: &sfu.NewPublisherSettings{ + StreamType: signaling.StreamTypeVideo, + PublisherSettings: &signaling.NewPublisherSettings{ Bitrate: 1234567, - MediaTypes: sfu.MediaTypeAudio | sfu.MediaTypeVideo, + MediaTypes: signaling.MediaTypeAudio | signaling.MediaTypeVideo, }, }, })) @@ -1532,12 +1177,12 @@ func TestProxyUnpublishRemotePublisherClosed(t *testing.T) { _, err = client2.RunUntilLoad(ctx, 0) assert.NoError(err) - require.NoError(client2.WriteJSON(&proxy.ClientMessage{ + require.NoError(client2.WriteJSON(&signaling.ProxyClientMessage{ Id: "3456", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "publish-remote", - StreamType: sfu.StreamTypeVideo, + StreamType: signaling.StreamTypeVideo, ClientId: clientId, Hostname: "remote-host", Port: 10001, @@ -1556,15 +1201,15 @@ func TestProxyUnpublishRemotePublisherClosed(t *testing.T) { assert.Equal(hello2.Hello.SessionId, publisher.getRemoteId()) if remoteData := publisher.getRemoteData(); assert.NotNil(remoteData) { assert.Equal("remote-host", remoteData.hostname) - assert.Equal(10001, remoteData.port) - assert.Equal(10002, remoteData.rtcpPort) + assert.EqualValues(10001, remoteData.port) + assert.EqualValues(10002, remoteData.rtcpPort) } } - require.NoError(client1.WriteJSON(&proxy.ClientMessage{ + require.NoError(client1.WriteJSON(&signaling.ProxyClientMessage{ Id: "4567", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "delete-publisher", ClientId: clientId, }, @@ -1582,14 +1227,14 @@ func TestProxyUnpublishRemotePublisherClosed(t *testing.T) { assert.Equal(hello2.Hello.SessionId, publisher.getRemoteId()) if remoteData := publisher.getRemoteData(); assert.NotNil(remoteData) { assert.Equal("remote-host", remoteData.hostname) - assert.Equal(10001, remoteData.port) - assert.Equal(10002, remoteData.rtcpPort) + assert.EqualValues(10001, remoteData.port) + assert.EqualValues(10002, remoteData.rtcpPort) } } // ...but the session no longer contains information on the remote publisher. - if data, err := proxyServer.cookie.DecodePublic(hello2.Hello.SessionId); assert.NoError(err) { - session := proxyServer.GetSession(data.Sid) + if data, err := proxy.cookie.DecodePublic(hello2.Hello.SessionId); assert.NoError(err) { + session := proxy.GetSession(data.Sid) if assert.NotNil(session) { session.remotePublishersLock.Lock() defer session.remotePublishersLock.Unlock() @@ -1603,13 +1248,13 @@ func TestProxyUnpublishRemotePublisherClosed(t *testing.T) { } func TestProxyUnpublishRemoteOnSessionClose(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) + proxy, key, server := newProxyServerForTest(t) mcu := NewUnpublishRemoteTestMCU(t) - proxyServer.mcu = mcu + proxy.mcu = mcu ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() @@ -1626,18 +1271,18 @@ func TestProxyUnpublishRemoteOnSessionClose(t *testing.T) { _, err := client1.RunUntilLoad(ctx, 0) assert.NoError(err) - publisherId := api.PublicSessionId("the-publisher-id") - require.NoError(client1.WriteJSON(&proxy.ClientMessage{ + publisherId := "the-publisher-id" + require.NoError(client1.WriteJSON(&signaling.ProxyClientMessage{ Id: "2345", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "create-publisher", PublisherId: publisherId, Sid: "1234-abcd", - StreamType: sfu.StreamTypeVideo, - PublisherSettings: &sfu.NewPublisherSettings{ + StreamType: signaling.StreamTypeVideo, + PublisherSettings: &signaling.NewPublisherSettings{ Bitrate: 1234567, - MediaTypes: sfu.MediaTypeAudio | sfu.MediaTypeVideo, + MediaTypes: signaling.MediaTypeAudio | signaling.MediaTypeVideo, }, }, })) @@ -1664,12 +1309,12 @@ func TestProxyUnpublishRemoteOnSessionClose(t *testing.T) { _, err = client2.RunUntilLoad(ctx, 0) assert.NoError(err) - require.NoError(client2.WriteJSON(&proxy.ClientMessage{ + require.NoError(client2.WriteJSON(&signaling.ProxyClientMessage{ Id: "3456", Type: "command", - Command: &proxy.CommandClientMessage{ + Command: &signaling.CommandProxyClientMessage{ Type: "publish-remote", - StreamType: sfu.StreamTypeVideo, + StreamType: signaling.StreamTypeVideo, ClientId: clientId, Hostname: "remote-host", Port: 10001, @@ -1688,8 +1333,8 @@ func TestProxyUnpublishRemoteOnSessionClose(t *testing.T) { assert.Equal(hello2.Hello.SessionId, publisher.getRemoteId()) if remoteData := publisher.getRemoteData(); assert.NotNil(remoteData) { assert.Equal("remote-host", remoteData.hostname) - assert.Equal(10001, remoteData.port) - assert.Equal(10002, remoteData.rtcpPort) + assert.EqualValues(10001, remoteData.port) + assert.EqualValues(10002, remoteData.rtcpPort) } } @@ -1701,332 +1346,3 @@ func TestProxyUnpublishRemoteOnSessionClose(t *testing.T) { assert.Nil(publisher.getRemoteData()) } } - -func TestExpireSessions(t *testing.T) { - t.Parallel() - - synctest.Test(t, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - proxyServer, key, server := newProxyServerForTest(t) - server.Close() - - // No-op - proxyServer.expireSessions() - - claims := &proxy.TokenClaims{ - RegisteredClaims: jwt.RegisteredClaims{ - IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge / 2)), - Issuer: TokenIdForTest, - }, - } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - tokenString, err := token.SignedString(key) - require.NoError(err) - - hello := &proxy.HelloClientMessage{ - Version: "1.0", - Token: tokenString, - } - session, err := proxyServer.NewSession(hello) - require.NoError(err) - t.Cleanup(func() { - proxyServer.DeleteSession(session.Sid()) - }) - assert.Same(session, proxyServer.GetSession(session.Sid())) - - proxyServer.expireSessions() - assert.Same(session, proxyServer.GetSession(session.Sid())) - - time.Sleep(sessionExpirationTime) - proxyServer.expireSessions() - - assert.Nil(proxyServer.GetSession(session.Sid())) - }) -} - -func TestScheduleShutdownEmpty(t *testing.T) { - t.Parallel() - - proxyServer, _, _ := newProxyServerForTest(t) - - proxyServer.ScheduleShutdown() - <-proxyServer.ShutdownChannel() -} - -func TestScheduleShutdownNoClients(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - proxyServer, key, server := newProxyServerForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client := NewProxyTestClient(ctx, t, server.URL) - defer client.CloseWithBye() - - require.NoError(client.SendHello(key)) - - if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello) - } - - _, err := client.RunUntilLoad(ctx, 0) - assert.NoError(err) - - proxyServer.ScheduleShutdown() - - if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("event", msg.Type) - if event := msg.Event; assert.NotNil(event) { - assert.Equal("shutdown-scheduled", event.Type) - } - } - - <-proxyServer.ShutdownChannel() -} - -func TestScheduleShutdown(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - proxyServer, key, server := newProxyServerForTest(t) - - mcu := NewPublisherTestMCU(t) - proxyServer.mcu = mcu - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client := NewProxyTestClient(ctx, t, server.URL) - defer client.CloseWithBye() - - require.NoError(client.SendHello(key)) - - if hello, err := client.RunUntilHello(ctx); assert.NoError(err) { - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello) - } - - _, err := client.RunUntilLoad(ctx, 0) - assert.NoError(err) - - publisherId := api.PublicSessionId("the-publisher-id") - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "2345", - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "create-publisher", - PublisherId: publisherId, - Sid: "1234-abcd", - StreamType: sfu.StreamTypeVideo, - PublisherSettings: &sfu.NewPublisherSettings{ - Bitrate: 1234567, - MediaTypes: sfu.MediaTypeAudio | sfu.MediaTypeVideo, - }, - }, - })) - - var clientId string - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("2345", message.Id) - if err := checkMessageType(message, "command"); assert.NoError(err) { - require.NotEmpty(message.Command.Id) - clientId = message.Command.Id - } - } - - readyChan := make(chan struct{}) - var readyReceived atomic.Bool - go func() { - for { - select { - case <-proxyServer.ShutdownChannel(): - return - case <-readyChan: - readyReceived.Store(true) - case <-ctx.Done(): - assert.NoError(ctx.Err()) - return - } - } - }() - - proxyServer.ScheduleShutdown() - - if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("event", msg.Type) - if event := msg.Event; assert.NotNil(event) { - assert.Equal("shutdown-scheduled", event.Type) - } - } - close(readyChan) - - proxyServer.ScheduleShutdown() - - select { - case <-proxyServer.ShutdownChannel(): - assert.Fail("should only shutdown after all clients closed") - default: - } - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "4567", - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "delete-publisher", - ClientId: clientId, - }, - })) - - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("4567", message.Id) - if err := checkMessageType(message, "command"); assert.NoError(err) { - require.NotEmpty(message.Command.Id) - } - } - - <-proxyServer.ShutdownChannel() - assert.True(readyReceived.Load()) -} - -func TestScheduleShutdownOnResume(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - proxyServer, key, server := newProxyServerForTest(t) - - mcu := NewPublisherTestMCU(t) - proxyServer.mcu = mcu - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client := NewProxyTestClient(ctx, t, server.URL) - defer client.CloseWithBye() - - require.NoError(client.SendHello(key)) - - hello, err := client.RunUntilHello(ctx) - if assert.NoError(err) { - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello) - } - - _, err = client.RunUntilLoad(ctx, 0) - assert.NoError(err) - - publisherId := api.PublicSessionId("the-publisher-id") - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "2345", - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "create-publisher", - PublisherId: publisherId, - Sid: "1234-abcd", - StreamType: sfu.StreamTypeVideo, - PublisherSettings: &sfu.NewPublisherSettings{ - Bitrate: 1234567, - MediaTypes: sfu.MediaTypeAudio | sfu.MediaTypeVideo, - }, - }, - })) - - var clientId string - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("2345", message.Id) - if err := checkMessageType(message, "command"); assert.NoError(err) { - require.NotEmpty(message.Command.Id) - clientId = message.Command.Id - } - } - - readyChan := make(chan struct{}) - var readyReceived atomic.Bool - go func() { - for { - select { - case <-proxyServer.ShutdownChannel(): - return - case <-readyChan: - readyReceived.Store(true) - case <-ctx.Done(): - assert.NoError(ctx.Err()) - return - } - } - }() - - client.Close() - - proxyServer.ScheduleShutdown() - - client = NewProxyTestClient(ctx, t, server.URL) - defer client.CloseWithBye() - - hello2 := &proxy.ClientMessage{ - Id: "1234", - Type: "hello", - Hello: &proxy.HelloClientMessage{ - Version: "1.0", - Features: []string{}, - ResumeId: hello.Hello.SessionId, - }, - } - require.NoError(client.WriteJSON(hello2)) - - if hello3, err := client.RunUntilHello(ctx); assert.NoError(err) { - assert.Equal(hello.Hello.SessionId, hello3.Hello.SessionId) - } - - if msg, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("event", msg.Type) - if event := msg.Event; assert.NotNil(event) { - assert.Equal("shutdown-scheduled", event.Type) - } - } - - client2 := NewProxyTestClient(ctx, t, server.URL) - defer client2.CloseWithBye() - - require.NoError(client2.SendHello(key)) - - if hello, err := client2.RunUntilHello(ctx); assert.NoError(err) { - assert.NotEmpty(hello.Hello.SessionId, "%+v", hello) - } - - if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("event", msg.Type) - if event := msg.Event; assert.NotNil(event) { - assert.Equal("shutdown-scheduled", event.Type) - } - } - close(readyChan) - - proxyServer.ScheduleShutdown() - - select { - case <-proxyServer.ShutdownChannel(): - assert.Fail("should only shutdown after all clients closed") - default: - } - require.NoError(client.WriteJSON(&proxy.ClientMessage{ - Id: "4567", - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "delete-publisher", - ClientId: clientId, - }, - })) - - if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { - assert.Equal("4567", message.Id) - if err := checkMessageType(message, "command"); assert.NoError(err) { - require.NotEmpty(message.Command.Id) - } - } - - <-proxyServer.ShutdownChannel() - assert.True(readyReceived.Load()) -} diff --git a/cmd/proxy/proxy_session.go b/proxy/proxy_session.go similarity index 59% rename from cmd/proxy/proxy_session.go rename to proxy/proxy_session.go index d5345b1..de6645b 100644 --- a/cmd/proxy/proxy_session.go +++ b/proxy/proxy_session.go @@ -24,14 +24,12 @@ package main import ( "context" "fmt" + "log" "sync" "sync/atomic" "time" - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) const ( @@ -40,59 +38,49 @@ const ( ) type remotePublisherData struct { - id api.PublicSessionId hostname string port int rtcpPort int } type ProxySession struct { - logger log.Logger proxy *ProxyServer - id api.PublicSessionId + id string sid uint64 lastUsed atomic.Int64 ctx context.Context closeFunc context.CancelFunc - clientLock sync.Mutex - // +checklocks:clientLock - client *ProxyClient - // +checklocks:clientLock - pendingMessages []*proxy.ServerMessage + clientLock sync.Mutex + client *ProxyClient + pendingMessages []*signaling.ProxyServerMessage publishersLock sync.Mutex - // +checklocks:publishersLock - publishers map[string]sfu.Publisher - // +checklocks:publishersLock - publisherIds map[sfu.Publisher]string + publishers map[string]signaling.McuPublisher + publisherIds map[signaling.McuPublisher]string subscribersLock sync.Mutex - // +checklocks:subscribersLock - subscribers map[string]sfu.Subscriber - // +checklocks:subscribersLock - subscriberIds map[sfu.Subscriber]string + subscribers map[string]signaling.McuSubscriber + subscriberIds map[signaling.McuSubscriber]string remotePublishersLock sync.Mutex - // +checklocks:remotePublishersLock - remotePublishers map[sfu.RemoteAwarePublisher]map[string]*remotePublisherData + remotePublishers map[signaling.McuPublisher]map[string]*remotePublisherData } -func NewProxySession(proxy *ProxyServer, sid uint64, id api.PublicSessionId) *ProxySession { +func NewProxySession(proxy *ProxyServer, sid uint64, id string) *ProxySession { ctx, closeFunc := context.WithCancel(context.Background()) result := &ProxySession{ - logger: proxy.logger, proxy: proxy, id: id, sid: sid, ctx: ctx, closeFunc: closeFunc, - publishers: make(map[string]sfu.Publisher), - publisherIds: make(map[sfu.Publisher]string), + publishers: make(map[string]signaling.McuPublisher), + publisherIds: make(map[signaling.McuPublisher]string), - subscribers: make(map[string]sfu.Subscriber), - subscriberIds: make(map[sfu.Subscriber]string), + subscribers: make(map[string]signaling.McuSubscriber), + subscriberIds: make(map[signaling.McuSubscriber]string), } result.MarkUsed() return result @@ -102,7 +90,7 @@ func (s *ProxySession) Context() context.Context { return s.ctx } -func (s *ProxySession) PublicId() api.PublicSessionId { +func (s *ProxySession) PublicId() string { return s.id } @@ -117,7 +105,7 @@ func (s *ProxySession) LastUsed() time.Time { func (s *ProxySession) IsExpired() bool { expiresAt := s.LastUsed().Add(sessionExpirationTime) - return !expiresAt.After(time.Now()) + return expiresAt.Before(time.Now()) } func (s *ProxySession) MarkUsed() { @@ -132,9 +120,9 @@ func (s *ProxySession) Close() { if s.IsExpired() { reason = "session_expired" } - prev.SendMessage(&proxy.ServerMessage{ + prev.SendMessage(&signaling.ProxyServerMessage{ Type: "bye", - Bye: &proxy.ByeServerMessage{ + Bye: &signaling.ByeProxyServerMessage{ Reason: reason, }, }) @@ -151,7 +139,7 @@ func (s *ProxySession) SetClient(client *ProxyClient) *ProxyClient { s.clientLock.Lock() prev := s.client s.client = client - var messages []*proxy.ServerMessage + var messages []*signaling.ProxyServerMessage if client != nil { messages, s.pendingMessages = s.pendingMessages, nil } @@ -169,19 +157,19 @@ func (s *ProxySession) SetClient(client *ProxyClient) *ProxyClient { return prev } -func (s *ProxySession) OnUpdateOffer(client sfu.Client, offer api.StringMap) { +func (s *ProxySession) OnUpdateOffer(client signaling.McuClient, offer map[string]interface{}) { id := s.proxy.GetClientId(client) if id == "" { - s.logger.Printf("Received offer %+v from unknown %s client %s (%+v)", offer, client.StreamType(), client.Id(), client) + log.Printf("Received offer %+v from unknown %s client %s (%+v)", offer, client.StreamType(), client.Id(), client) return } - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "payload", - Payload: &proxy.PayloadServerMessage{ + Payload: &signaling.PayloadProxyServerMessage{ Type: "offer", ClientId: id, - Payload: api.StringMap{ + Payload: map[string]interface{}{ "offer": offer, }, }, @@ -189,19 +177,19 @@ func (s *ProxySession) OnUpdateOffer(client sfu.Client, offer api.StringMap) { s.sendMessage(msg) } -func (s *ProxySession) OnIceCandidate(client sfu.Client, candidate any) { +func (s *ProxySession) OnIceCandidate(client signaling.McuClient, candidate interface{}) { id := s.proxy.GetClientId(client) if id == "" { - s.logger.Printf("Received candidate %+v from unknown %s client %s (%+v)", candidate, client.StreamType(), client.Id(), client) + log.Printf("Received candidate %+v from unknown %s client %s (%+v)", candidate, client.StreamType(), client.Id(), client) return } - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "payload", - Payload: &proxy.PayloadServerMessage{ + Payload: &signaling.PayloadProxyServerMessage{ Type: "candidate", ClientId: id, - Payload: api.StringMap{ + Payload: map[string]interface{}{ "candidate": candidate, }, }, @@ -209,7 +197,7 @@ func (s *ProxySession) OnIceCandidate(client sfu.Client, candidate any) { s.sendMessage(msg) } -func (s *ProxySession) sendMessage(message *proxy.ServerMessage) { +func (s *ProxySession) sendMessage(message *signaling.ProxyServerMessage) { var client *ProxyClient s.clientLock.Lock() client = s.client @@ -222,16 +210,16 @@ func (s *ProxySession) sendMessage(message *proxy.ServerMessage) { } } -func (s *ProxySession) OnIceCompleted(client sfu.Client) { +func (s *ProxySession) OnIceCompleted(client signaling.McuClient) { id := s.proxy.GetClientId(client) if id == "" { - s.logger.Printf("Received ice completed event from unknown %s client %s (%+v)", client.StreamType(), client.Id(), client) + log.Printf("Received ice completed event from unknown %s client %s (%+v)", client.StreamType(), client.Id(), client) return } - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "ice-completed", ClientId: id, }, @@ -239,16 +227,16 @@ func (s *ProxySession) OnIceCompleted(client sfu.Client) { s.sendMessage(msg) } -func (s *ProxySession) SubscriberSidUpdated(subscriber sfu.Subscriber) { +func (s *ProxySession) SubscriberSidUpdated(subscriber signaling.McuSubscriber) { id := s.proxy.GetClientId(subscriber) if id == "" { - s.logger.Printf("Received subscriber sid updated event from unknown %s subscriber %s (%+v)", subscriber.StreamType(), subscriber.Id(), subscriber) + log.Printf("Received subscriber sid updated event from unknown %s subscriber %s (%+v)", subscriber.StreamType(), subscriber.Id(), subscriber) return } - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "subscriber-sid-updated", ClientId: id, Sid: subscriber.Sid(), @@ -257,15 +245,15 @@ func (s *ProxySession) SubscriberSidUpdated(subscriber sfu.Subscriber) { s.sendMessage(msg) } -func (s *ProxySession) PublisherClosed(publisher sfu.Publisher) { +func (s *ProxySession) PublisherClosed(publisher signaling.McuPublisher) { if id := s.DeletePublisher(publisher); id != "" { if s.proxy.DeleteClient(id, publisher) { statsPublishersCurrent.WithLabelValues(string(publisher.StreamType())).Dec() } - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "publisher-closed", ClientId: id, }, @@ -274,15 +262,15 @@ func (s *ProxySession) PublisherClosed(publisher sfu.Publisher) { } } -func (s *ProxySession) SubscriberClosed(subscriber sfu.Subscriber) { +func (s *ProxySession) SubscriberClosed(subscriber signaling.McuSubscriber) { if id := s.DeleteSubscriber(subscriber); id != "" { if s.proxy.DeleteClient(id, subscriber) { statsSubscribersCurrent.WithLabelValues(string(subscriber.StreamType())).Dec() } - msg := &proxy.ServerMessage{ + msg := &signaling.ProxyServerMessage{ Type: "event", - Event: &proxy.EventServerMessage{ + Event: &signaling.EventProxyServerMessage{ Type: "subscriber-closed", ClientId: id, }, @@ -291,7 +279,7 @@ func (s *ProxySession) SubscriberClosed(subscriber sfu.Subscriber) { } } -func (s *ProxySession) StorePublisher(ctx context.Context, id string, publisher sfu.Publisher) { +func (s *ProxySession) StorePublisher(ctx context.Context, id string, publisher signaling.McuPublisher) { s.publishersLock.Lock() defer s.publishersLock.Unlock() @@ -299,7 +287,7 @@ func (s *ProxySession) StorePublisher(ctx context.Context, id string, publisher s.publisherIds[publisher] = id } -func (s *ProxySession) DeletePublisher(publisher sfu.Publisher) string { +func (s *ProxySession) DeletePublisher(publisher signaling.McuPublisher) string { s.publishersLock.Lock() defer s.publishersLock.Unlock() @@ -310,16 +298,12 @@ func (s *ProxySession) DeletePublisher(publisher sfu.Publisher) string { delete(s.publishers, id) delete(s.publisherIds, publisher) - if rp, ok := publisher.(sfu.RemoteAwarePublisher); ok { - s.remotePublishersLock.Lock() - defer s.remotePublishersLock.Unlock() - delete(s.remotePublishers, rp) - } + delete(s.remotePublishers, publisher) go s.proxy.PublisherDeleted(publisher) return id } -func (s *ProxySession) StoreSubscriber(ctx context.Context, id string, subscriber sfu.Subscriber) { +func (s *ProxySession) StoreSubscriber(ctx context.Context, id string, subscriber signaling.McuSubscriber) { s.subscribersLock.Lock() defer s.subscribersLock.Unlock() @@ -327,7 +311,7 @@ func (s *ProxySession) StoreSubscriber(ctx context.Context, id string, subscribe s.subscriberIds[subscriber] = id } -func (s *ProxySession) DeleteSubscriber(subscriber sfu.Subscriber) string { +func (s *ProxySession) DeleteSubscriber(subscriber signaling.McuSubscriber) string { s.subscribersLock.Lock() defer s.subscribersLock.Unlock() @@ -345,7 +329,7 @@ func (s *ProxySession) clearPublishers() { s.publishersLock.Lock() defer s.publishersLock.Unlock() - go func(publishers map[string]sfu.Publisher) { + go func(publishers map[string]signaling.McuPublisher) { for id, publisher := range publishers { if s.proxy.DeleteClient(id, publisher) { statsPublishersCurrent.WithLabelValues(string(publisher.StreamType())).Dec() @@ -354,7 +338,7 @@ func (s *ProxySession) clearPublishers() { } }(s.publishers) // Can't use clear(...) here as the map is processed by the goroutine above. - s.publishers = make(map[string]sfu.Publisher) + s.publishers = make(map[string]signaling.McuPublisher) clear(s.publisherIds) } @@ -362,11 +346,11 @@ func (s *ProxySession) clearRemotePublishers() { s.remotePublishersLock.Lock() defer s.remotePublishersLock.Unlock() - go func(remotePublishers map[sfu.RemoteAwarePublisher]map[string]*remotePublisherData) { + go func(remotePublishers map[signaling.McuPublisher]map[string]*remotePublisherData) { for publisher, entries := range remotePublishers { for _, data := range entries { if err := publisher.UnpublishRemote(context.Background(), s.PublicId(), data.hostname, data.port, data.rtcpPort); err != nil { - s.logger.Printf("Error unpublishing %s %s from remote %s: %s", publisher.StreamType(), publisher.Id(), data.hostname, err) + log.Printf("Error unpublishing %s %s from remote %s: %s", publisher.StreamType(), publisher.Id(), data.hostname, err) } } } @@ -375,10 +359,10 @@ func (s *ProxySession) clearRemotePublishers() { } func (s *ProxySession) clearSubscribers() { - s.subscribersLock.Lock() - defer s.subscribersLock.Unlock() + s.publishersLock.Lock() + defer s.publishersLock.Unlock() - go func(subscribers map[string]sfu.Subscriber) { + go func(subscribers map[string]signaling.McuSubscriber) { for id, subscriber := range subscribers { if s.proxy.DeleteClient(id, subscriber) { statsSubscribersCurrent.WithLabelValues(string(subscriber.StreamType())).Dec() @@ -387,7 +371,7 @@ func (s *ProxySession) clearSubscribers() { } }(s.subscribers) // Can't use clear(...) here as the map is processed by the goroutine above. - s.subscribers = make(map[string]sfu.Subscriber) + s.subscribers = make(map[string]signaling.McuSubscriber) clear(s.subscriberIds) } @@ -397,7 +381,7 @@ func (s *ProxySession) NotifyDisconnected() { s.clearRemotePublishers() } -func (s *ProxySession) AddRemotePublisher(publisher sfu.RemoteAwarePublisher, hostname string, port int, rtcpPort int) bool { +func (s *ProxySession) AddRemotePublisher(publisher signaling.McuPublisher, hostname string, port int, rtcpPort int) bool { s.remotePublishersLock.Lock() defer s.remotePublishersLock.Unlock() @@ -405,7 +389,7 @@ func (s *ProxySession) AddRemotePublisher(publisher sfu.RemoteAwarePublisher, ho if !found { remote = make(map[string]*remotePublisherData) if s.remotePublishers == nil { - s.remotePublishers = make(map[sfu.RemoteAwarePublisher]map[string]*remotePublisherData) + s.remotePublishers = make(map[signaling.McuPublisher]map[string]*remotePublisherData) } s.remotePublishers[publisher] = remote } @@ -416,7 +400,6 @@ func (s *ProxySession) AddRemotePublisher(publisher sfu.RemoteAwarePublisher, ho } data := &remotePublisherData{ - id: publisher.PublisherId(), hostname: hostname, port: port, rtcpPort: rtcpPort, @@ -425,7 +408,7 @@ func (s *ProxySession) AddRemotePublisher(publisher sfu.RemoteAwarePublisher, ho return true } -func (s *ProxySession) RemoveRemotePublisher(publisher sfu.RemoteAwarePublisher, hostname string, port int, rtcpPort int) { +func (s *ProxySession) RemoveRemotePublisher(publisher signaling.McuPublisher, hostname string, port int, rtcpPort int) { s.remotePublishersLock.Lock() defer s.remotePublishersLock.Unlock() @@ -444,43 +427,9 @@ func (s *ProxySession) RemoveRemotePublisher(publisher sfu.RemoteAwarePublisher, } } -func (s *ProxySession) OnPublisherDeleted(publisher sfu.Publisher) { - if publisher, ok := publisher.(sfu.RemoteAwarePublisher); ok { - s.OnRemoteAwarePublisherDeleted(publisher) - } -} - -func (s *ProxySession) OnRemoteAwarePublisherDeleted(publisher sfu.RemoteAwarePublisher) { +func (s *ProxySession) OnPublisherDeleted(publisher signaling.McuPublisher) { s.remotePublishersLock.Lock() defer s.remotePublishersLock.Unlock() - if entries, found := s.remotePublishers[publisher]; found { - delete(s.remotePublishers, publisher) - - for _, entry := range entries { - msg := &proxy.ServerMessage{ - Type: "event", - Event: &proxy.EventServerMessage{ - Type: "publisher-closed", - ClientId: string(entry.id), - }, - } - s.sendMessage(msg) - } - } -} - -func (s *ProxySession) OnRemotePublisherDeleted(publisherId api.PublicSessionId) { - s.subscribersLock.Lock() - defer s.subscribersLock.Unlock() - - for id, sub := range s.subscribers { - if sub.Publisher() == publisherId { - delete(s.subscribers, id) - delete(s.subscriberIds, sub) - - s.logger.Printf("Remote subscriber %s was closed, closing %s subscriber %s", publisherId, sub.StreamType(), sub.Id()) - go sub.Close(context.Background()) - } - } + delete(s.remotePublishers, publisher) } diff --git a/cmd/proxy/proxy_stats_prometheus.go b/proxy/proxy_stats_prometheus.go similarity index 94% rename from cmd/proxy/proxy_stats_prometheus.go rename to proxy/proxy_stats_prometheus.go index 26e317c..054ec2b 100644 --- a/cmd/proxy/proxy_stats_prometheus.go +++ b/proxy/proxy_stats_prometheus.go @@ -86,12 +86,6 @@ var ( Name: "token_errors_total", Help: "The total number of token errors", }, []string{"reason"}) - statsLoadCurrent = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "proxy", - Name: "load", - Help: "The current load of the signaling proxy", - }) ) func init() { @@ -105,5 +99,4 @@ func init() { prometheus.MustRegister(statsCommandMessagesTotal) prometheus.MustRegister(statsPayloadMessagesTotal) prometheus.MustRegister(statsTokenErrorsTotal) - prometheus.MustRegister(statsLoadCurrent) } diff --git a/cmd/proxy/proxy_testclient_test.go b/proxy/proxy_testclient_test.go similarity index 85% rename from cmd/proxy/proxy_testclient_test.go rename to proxy/proxy_testclient_test.go index 0eae10a..c08b5f3 100644 --- a/cmd/proxy/proxy_testclient_test.go +++ b/proxy/proxy_testclient_test.go @@ -34,9 +34,7 @@ import ( "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) var ( @@ -45,16 +43,15 @@ var ( type ProxyTestClient struct { t *testing.T - assert *assert.Assertions // +checklocksignore: Only written to from constructor. + assert *assert.Assertions require *require.Assertions - mu sync.Mutex - // +checklocks:mu + mu sync.Mutex conn *websocket.Conn messageChan chan []byte readErrorChan chan error - sessionId api.PublicSessionId + sessionId string } func NewProxyTestClient(ctx context.Context, t *testing.T, url string) *ProxyTestClient { @@ -120,16 +117,16 @@ loop: } func (c *ProxyTestClient) SendBye() error { - hello := &proxy.ClientMessage{ + hello := &signaling.ProxyClientMessage{ Id: "9876", Type: "bye", - Bye: &proxy.ByeClientMessage{}, + Bye: &signaling.ByeProxyClientMessage{}, } return c.WriteJSON(hello) } -func (c *ProxyTestClient) WriteJSON(data any) error { - if msg, ok := data.(*proxy.ClientMessage); ok { +func (c *ProxyTestClient) WriteJSON(data interface{}) error { + if msg, ok := data.(*signaling.ProxyClientMessage); ok { if err := msg.CheckValid(); err != nil { return err } @@ -140,11 +137,11 @@ func (c *ProxyTestClient) WriteJSON(data any) error { return c.conn.WriteJSON(data) } -func (c *ProxyTestClient) RunUntilMessage(ctx context.Context) (message *proxy.ServerMessage, err error) { +func (c *ProxyTestClient) RunUntilMessage(ctx context.Context) (message *signaling.ProxyServerMessage, err error) { select { case err = <-c.readErrorChan: case msg := <-c.messageChan: - var m proxy.ServerMessage + var m signaling.ProxyServerMessage if err = json.Unmarshal(msg, &m); err == nil { message = &m } @@ -165,7 +162,7 @@ func checkUnexpectedClose(err error) error { return nil } -func checkMessageType(message *proxy.ServerMessage, expectedType string) error { +func checkMessageType(message *signaling.ProxyServerMessage, expectedType string) error { if message == nil { return ErrNoMessageReceived } @@ -191,8 +188,8 @@ func checkMessageType(message *proxy.ServerMessage, expectedType string) error { return nil } -func (c *ProxyTestClient) SendHello(key any) error { - claims := &proxy.TokenClaims{ +func (c *ProxyTestClient) SendHello(key interface{}) error { + claims := &signaling.TokenClaims{ RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-maxTokenAge / 2)), Issuer: TokenIdForTest, @@ -202,10 +199,10 @@ func (c *ProxyTestClient) SendHello(key any) error { tokenString, err := token.SignedString(key) c.require.NoError(err) - hello := &proxy.ClientMessage{ + hello := &signaling.ProxyClientMessage{ Id: "1234", Type: "hello", - Hello: &proxy.HelloClientMessage{ + Hello: &signaling.HelloProxyClientMessage{ Version: "1.0", Features: []string{}, Token: tokenString, @@ -214,7 +211,7 @@ func (c *ProxyTestClient) SendHello(key any) error { return c.WriteJSON(hello) } -func (c *ProxyTestClient) RunUntilHello(ctx context.Context) (message *proxy.ServerMessage, err error) { +func (c *ProxyTestClient) RunUntilHello(ctx context.Context) (message *signaling.ProxyServerMessage, err error) { if message, err = c.RunUntilMessage(ctx); err != nil { return nil, err } @@ -228,7 +225,7 @@ func (c *ProxyTestClient) RunUntilHello(ctx context.Context) (message *proxy.Ser return message, nil } -func (c *ProxyTestClient) RunUntilLoad(ctx context.Context, load uint64) (message *proxy.ServerMessage, err error) { +func (c *ProxyTestClient) RunUntilLoad(ctx context.Context, load int64) (message *signaling.ProxyServerMessage, err error) { if message, err = c.RunUntilMessage(ctx); err != nil { return nil, err } @@ -247,8 +244,8 @@ func (c *ProxyTestClient) RunUntilLoad(ctx context.Context, load uint64) (messag return message, nil } -func (c *ProxyTestClient) SendCommand(command *proxy.CommandClientMessage) error { - message := &proxy.ClientMessage{ +func (c *ProxyTestClient) SendCommand(command *signaling.CommandProxyClientMessage) error { + message := &signaling.ProxyClientMessage{ Id: "2345", Type: "command", Command: command, diff --git a/cmd/proxy/proxy_tokens.go b/proxy/proxy_tokens.go similarity index 100% rename from cmd/proxy/proxy_tokens.go rename to proxy/proxy_tokens.go diff --git a/cmd/proxy/proxy_tokens_etcd.go b/proxy/proxy_tokens_etcd.go similarity index 77% rename from cmd/proxy/proxy_tokens_etcd.go rename to proxy/proxy_tokens_etcd.go index 84dc66a..09dfc51 100644 --- a/cmd/proxy/proxy_tokens_etcd.go +++ b/proxy/proxy_tokens_etcd.go @@ -24,8 +24,8 @@ package main import ( "bytes" "context" - "errors" "fmt" + "log" "strings" "sync/atomic" "time" @@ -33,9 +33,7 @@ import ( "github.com/dlintw/goconf" "github.com/golang-jwt/jwt/v5" - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) const ( @@ -48,27 +46,25 @@ type tokenCacheEntry struct { } type tokensEtcd struct { - logger log.Logger - client etcd.Client + client *signaling.EtcdClient tokenFormats atomic.Value - tokenCache *container.LruCache[*tokenCacheEntry] + tokenCache *signaling.LruCache } -func NewProxyTokensEtcd(logger log.Logger, config *goconf.ConfigFile) (ProxyTokens, error) { - client, err := etcd.NewClient(logger, config, "tokens") +func NewProxyTokensEtcd(config *goconf.ConfigFile) (ProxyTokens, error) { + client, err := signaling.NewEtcdClient(config, "tokens") if err != nil { return nil, err } if !client.IsConfigured() { - return nil, errors.New("no etcd endpoints configured") + return nil, fmt.Errorf("No etcd endpoints configured") } result := &tokensEtcd{ - logger: logger, client: client, - tokenCache: container.NewLruCache[*tokenCacheEntry](tokenCacheSize), + tokenCache: signaling.NewLruCache(tokenCacheSize), } if err := result.load(config, false); err != nil { return nil, err @@ -98,11 +94,11 @@ func (t *tokensEtcd) getByKey(id string, key string) (*ProxyToken, error) { if len(resp.Kvs) == 0 { return nil, nil } else if len(resp.Kvs) > 1 { - t.logger.Printf("Received multiple keys for %s, using last", key) + log.Printf("Received multiple keys for %s, using last", key) } keyValue := resp.Kvs[len(resp.Kvs)-1].Value - cached := t.tokenCache.Get(key) + cached, _ := t.tokenCache.Get(key).(*tokenCacheEntry) if cached == nil || !bytes.Equal(cached.keyValue, keyValue) { // Parsed public keys are cached to avoid the parse overhead. publicKey, err := jwt.ParseRSAPublicKeyFromPEM(keyValue) @@ -127,7 +123,7 @@ func (t *tokensEtcd) Get(id string) (*ProxyToken, error) { for _, k := range t.getKeys(id) { token, err := t.getByKey(id, k) if err != nil { - t.logger.Printf("Could not get public key from %s for %s: %s", k, id, err) + log.Printf("Could not get public key from %s for %s: %s", k, id, err) continue } else if token == nil { continue @@ -155,18 +151,18 @@ func (t *tokensEtcd) load(config *goconf.ConfigFile, ignoreErrors bool) error { } t.tokenFormats.Store(tokenFormats) - t.logger.Printf("Using %v as token formats", tokenFormats) + log.Printf("Using %v as token formats", tokenFormats) return nil } func (t *tokensEtcd) Reload(config *goconf.ConfigFile) { if err := t.load(config, true); err != nil { - t.logger.Printf("Error reloading etcd tokens: %s", err) + log.Printf("Error reloading etcd tokens: %s", err) } } func (t *tokensEtcd) Close() { if err := t.client.Close(); err != nil { - t.logger.Printf("Error while closing etcd client: %s", err) + log.Printf("Error while closing etcd client: %s", err) } } diff --git a/cmd/proxy/proxy_tokens_etcd_test.go b/proxy/proxy_tokens_etcd_test.go similarity index 73% rename from cmd/proxy/proxy_tokens_etcd_test.go rename to proxy/proxy_tokens_etcd_test.go index a278801..957f7d5 100644 --- a/cmd/proxy/proxy_tokens_etcd_test.go +++ b/proxy/proxy_tokens_etcd_test.go @@ -27,10 +27,13 @@ import ( "crypto/rsa" "crypto/x509" "encoding/pem" + "errors" "net" "net/url" "os" + "runtime" "strconv" + "syscall" "testing" "github.com/dlintw/goconf" @@ -38,23 +41,38 @@ import ( "github.com/stretchr/testify/require" "go.etcd.io/etcd/server/v3/embed" "go.etcd.io/etcd/server/v3/lease" - "go.uber.org/zap" - "go.uber.org/zap/zaptest" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) var ( etcdListenUrl = "http://localhost:8080" ) +func isErrorAddressAlreadyInUse(err error) bool { + var eOsSyscall *os.SyscallError + if !errors.As(err, &eOsSyscall) { + return false + } + var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr) + if !errors.As(eOsSyscall, &errErrno) { + return false + } + if errErrno == syscall.EADDRINUSE { + return true + } + const WSAEADDRINUSE = 10048 + if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE { + return true + } + return false +} + func newEtcdForTesting(t *testing.T) *embed.Etcd { cfg := embed.NewConfig() cfg.Dir = t.TempDir() os.Chmod(cfg.Dir, 0700) // nolint cfg.LogLevel = "warn" - cfg.ZapLoggerBuilder = embed.NewZapLoggerBuilder(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel))) u, err := url.Parse(etcdListenUrl) require.NoError(t, err) @@ -71,7 +89,7 @@ func newEtcdForTesting(t *testing.T) *embed.Etcd { peerListener.Host = net.JoinHostPort("localhost", strconv.Itoa(port+2)) cfg.ListenPeerUrls = []url.URL{*peerListener} etcd, err = embed.StartEtcd(cfg) - if test.IsErrorAddressAlreadyInUse(err) { + if isErrorAddressAlreadyInUse(err) { continue } @@ -97,8 +115,7 @@ func newTokensEtcdForTesting(t *testing.T) (*tokensEtcd, *embed.Etcd) { cfg.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String()) cfg.AddOption("tokens", "keyformat", "/%s, /testing/%s/key") - logger := logtest.NewLoggerForTest(t) - tokens, err := NewProxyTokensEtcd(logger, cfg) + tokens, err := NewProxyTokensEtcd(cfg) require.NoError(t, err) t.Cleanup(func() { tokens.Close() @@ -115,7 +132,7 @@ func storeKey(t *testing.T, etcd *embed.Etcd, key string, pubkey crypto.PublicKe data, err = x509.MarshalPKIXPublicKey(&pubkey) require.NoError(t, err) default: - require.Fail(t, "unknown key type", "type %T in %+v", pubkey, pubkey) + require.Fail(t, "unknown key type %T in %+v", pubkey, pubkey) } data = pem.EncodeToMemory(&pem.Block{ @@ -138,7 +155,7 @@ func generateAndSaveKey(t *testing.T, etcd *embed.Etcd, name string) *rsa.Privat } func TestProxyTokensEtcd(t *testing.T) { - t.Parallel() + signaling.CatchLogForTest(t) assert := assert.New(t) tokens, etcd := newTokensEtcdForTesting(t) @@ -153,34 +170,3 @@ func TestProxyTokensEtcd(t *testing.T) { assert.True(key2.PublicKey.Equal(token.key)) } } - -func TestProxyTokensEtcdReload(t *testing.T) { - t.Parallel() - assert := assert.New(t) - tokens, etcd := newTokensEtcdForTesting(t) - - key1 := generateAndSaveKey(t, etcd, "/foo") - - if token, err := tokens.Get("foo"); assert.NoError(err) && assert.NotNil(token) { - assert.True(key1.PublicKey.Equal(token.key)) - } - - if token, err := tokens.Get("bar"); assert.NoError(err) { - assert.Nil(token) - } - - cfg := goconf.NewConfigFile() - cfg.AddOption("etcd", "endpoints", etcd.Config().ListenClientUrls[0].String()) - cfg.AddOption("tokens", "keyformat", "/reload/%s/key") - - tokens.Reload(cfg) - key2 := generateAndSaveKey(t, etcd, "/reload/bar/key") - - if token, err := tokens.Get("foo"); assert.NoError(err) { - assert.Nil(token) - } - - if token, err := tokens.Get("bar"); assert.NoError(err) && assert.NotNil(token) { - assert.True(key2.PublicKey.Equal(token.key)) - } -} diff --git a/cmd/proxy/proxy_tokens_static.go b/proxy/proxy_tokens_static.go similarity index 69% rename from cmd/proxy/proxy_tokens_static.go rename to proxy/proxy_tokens_static.go index 6e68509..8de255a 100644 --- a/cmd/proxy/proxy_tokens_static.go +++ b/proxy/proxy_tokens_static.go @@ -23,26 +23,23 @@ package main import ( "fmt" + "log" "os" - "slices" + "sort" "sync/atomic" "github.com/dlintw/goconf" "github.com/golang-jwt/jwt/v5" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" + signaling "github.com/strukturag/nextcloud-spreed-signaling" ) type tokensStatic struct { - logger log.Logger tokenKeys atomic.Value } -func NewProxyTokensStatic(logger log.Logger, config *goconf.ConfigFile) (ProxyTokens, error) { - result := &tokensStatic{ - logger: logger, - } +func NewProxyTokensStatic(config *goconf.ConfigFile) (ProxyTokens, error) { + result := &tokensStatic{} if err := result.load(config, false); err != nil { return nil, err } @@ -64,8 +61,8 @@ func (t *tokensStatic) Get(id string) (*ProxyToken, error) { return token, nil } -func (t *tokensStatic) load(cfg *goconf.ConfigFile, ignoreErrors bool) error { - options, err := config.GetStringOptions(cfg, "tokens", ignoreErrors) +func (t *tokensStatic) load(config *goconf.ConfigFile, ignoreErrors bool) error { + options, err := signaling.GetStringOptions(config, "tokens", ignoreErrors) if err != nil { return err } @@ -74,29 +71,29 @@ func (t *tokensStatic) load(cfg *goconf.ConfigFile, ignoreErrors bool) error { for id, filename := range options { if filename == "" { if !ignoreErrors { - return fmt.Errorf("no filename given for token %s", id) + return fmt.Errorf("No filename given for token %s", id) } - t.logger.Printf("No filename given for token %s, ignoring", id) + log.Printf("No filename given for token %s, ignoring", id) continue } keyData, err := os.ReadFile(filename) if err != nil { if !ignoreErrors { - return fmt.Errorf("could not read public key from %s: %w", filename, err) + return fmt.Errorf("Could not read public key from %s: %s", filename, err) } - t.logger.Printf("Could not read public key from %s, ignoring: %s", filename, err) + log.Printf("Could not read public key from %s, ignoring: %s", filename, err) continue } key, err := jwt.ParseRSAPublicKeyFromPEM(keyData) if err != nil { if !ignoreErrors { - return fmt.Errorf("could not parse public key from %s: %w", filename, err) + return fmt.Errorf("Could not parse public key from %s: %s", filename, err) } - t.logger.Printf("Could not parse public key from %s, ignoring: %s", filename, err) + log.Printf("Could not parse public key from %s, ignoring: %s", filename, err) continue } @@ -107,14 +104,14 @@ func (t *tokensStatic) load(cfg *goconf.ConfigFile, ignoreErrors bool) error { } if len(tokenKeys) == 0 { - t.logger.Printf("No token keys loaded") + log.Printf("No token keys loaded") } else { var keyIds []string for k := range tokenKeys { keyIds = append(keyIds, k) } - slices.Sort(keyIds) - t.logger.Printf("Enabled token keys: %v", keyIds) + sort.Strings(keyIds) + log.Printf("Enabled token keys: %v", keyIds) } t.setTokenKeys(tokenKeys) return nil @@ -122,7 +119,7 @@ func (t *tokensStatic) load(cfg *goconf.ConfigFile, ignoreErrors bool) error { func (t *tokensStatic) Reload(config *goconf.ConfigFile) { if err := t.load(config, true); err != nil { - t.logger.Printf("Error reloading static tokens: %s", err) + log.Printf("Error reloading static tokens: %s", err) } } diff --git a/sfu/proxy/config.go b/proxy_config.go similarity index 95% rename from sfu/proxy/config.go rename to proxy_config.go index 5453f97..2a4102c 100644 --- a/sfu/proxy/config.go +++ b/proxy_config.go @@ -19,13 +19,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package proxy +package signaling import ( "github.com/dlintw/goconf" ) -type Config interface { +type ProxyConfig interface { Start() error Stop() diff --git a/sfu/proxy/config_etcd.go b/proxy_config_etcd.go similarity index 52% rename from sfu/proxy/config_etcd.go rename to proxy_config_etcd.go index 2714231..35ccade 100644 --- a/sfu/proxy/config_etcd.go +++ b/proxy_config_etcd.go @@ -19,72 +19,49 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package proxy +package signaling import ( "context" "encoding/json" "errors" + "log" "sync" - "sync/atomic" "time" "github.com/dlintw/goconf" clientv3 "go.etcd.io/etcd/client/v3" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" ) -const ( - initialWaitDelay = time.Second - maxWaitDelay = 8 * time.Second -) +type proxyConfigEtcd struct { + mu sync.Mutex + proxy McuProxy -type configEtcd struct { - logger log.Logger - mu sync.Mutex - proxy McuProxy // +checklocksignore: Only written to from constructor. - - client etcd.Client + client *EtcdClient keyPrefix string - // +checklocks:mu - keyInfos map[string]*proxy.InformationEtcd - // +checklocks:mu - urlToKey map[string]string + keyInfos map[string]*ProxyInformationEtcd + urlToKey map[string]string closeCtx context.Context closeFunc context.CancelFunc - - initializing atomic.Bool - initializedCtx context.Context - initializedFunc context.CancelFunc - runningDone sync.WaitGroup } -func NewConfigEtcd(logger log.Logger, config *goconf.ConfigFile, etcdClient etcd.Client, sfuProxy McuProxy) (Config, error) { +func NewProxyConfigEtcd(config *goconf.ConfigFile, etcdClient *EtcdClient, proxy McuProxy) (ProxyConfig, error) { if !etcdClient.IsConfigured() { - return nil, errors.New("no etcd endpoints configured") + return nil, errors.New("No etcd endpoints configured") } - initializedCtx, initializedFunc := context.WithCancel(context.Background()) closeCtx, closeFunc := context.WithCancel(context.Background()) - result := &configEtcd{ - logger: logger, - proxy: sfuProxy, + result := &proxyConfigEtcd{ + proxy: proxy, client: etcdClient, - keyInfos: make(map[string]*proxy.InformationEtcd), + keyInfos: make(map[string]*ProxyInformationEtcd), urlToKey: make(map[string]string), closeCtx: closeCtx, closeFunc: closeFunc, - - initializedCtx: initializedCtx, - initializedFunc: initializedFunc, } if err := result.configure(config, false); err != nil { return nil, err @@ -92,7 +69,7 @@ func NewConfigEtcd(logger log.Logger, config *goconf.ConfigFile, etcdClient etcd return result, nil } -func (p *configEtcd) configure(config *goconf.ConfigFile, fromReload bool) error { +func (p *proxyConfigEtcd) configure(config *goconf.ConfigFile, fromReload bool) error { keyPrefix, _ := config.GetString("mcu", "keyprefix") if keyPrefix == "" { keyPrefix = "/%s" @@ -102,38 +79,23 @@ func (p *configEtcd) configure(config *goconf.ConfigFile, fromReload bool) error return nil } -func (p *configEtcd) Start() error { +func (p *proxyConfigEtcd) Start() error { p.client.AddListener(p) return nil } -func (p *configEtcd) Reload(config *goconf.ConfigFile) error { +func (p *proxyConfigEtcd) Reload(config *goconf.ConfigFile) error { // not implemented return nil } -func (p *configEtcd) Stop() { - firstStop := p.closeCtx.Err() == nil - p.closeFunc() +func (p *proxyConfigEtcd) Stop() { p.client.RemoveListener(p) - if firstStop { - if p.initializing.Load() { - <-p.initializedCtx.Done() - } - p.runningDone.Wait() - } + p.closeFunc() } -func (p *configEtcd) EtcdClientCreated(client etcd.Client) { - p.initializing.Store(true) - if p.closeCtx.Err() != nil { - // Stopped before etcd client was connected. - p.initializedFunc() - return - } - - p.runningDone.Go(func() { - defer p.initializedFunc() +func (p *proxyConfigEtcd) EtcdClientCreated(client *EtcdClient) { + go func() { if err := client.WaitForConnection(p.closeCtx); err != nil { if errors.Is(err, context.Canceled) { return @@ -142,7 +104,7 @@ func (p *configEtcd) EtcdClientCreated(client etcd.Client) { panic(err) } - backoff, err := async.NewExponentialBackoff(initialWaitDelay, maxWaitDelay) + backoff, err := NewExponentialBackoff(initialWaitDelay, maxWaitDelay) if err != nil { panic(err) } @@ -154,9 +116,9 @@ func (p *configEtcd) EtcdClientCreated(client etcd.Client) { if errors.Is(err, context.Canceled) { return } else if errors.Is(err, context.DeadlineExceeded) { - p.logger.Printf("Timeout getting initial list of proxy URLs, retry in %s", backoff.NextWait()) + log.Printf("Timeout getting initial list of proxy URLs, retry in %s", backoff.NextWait()) } else { - p.logger.Printf("Could not get initial list of proxy URLs, retry in %s: %s", backoff.NextWait(), err) + log.Printf("Could not get initial list of proxy URLs, retry in %s: %s", backoff.NextWait(), err) } backoff.Wait(p.closeCtx) @@ -169,14 +131,13 @@ func (p *configEtcd) EtcdClientCreated(client etcd.Client) { nextRevision = response.Header.Revision + 1 break } - p.initializedFunc() prevRevision := nextRevision backoff.Reset() for p.closeCtx.Err() == nil { var err error if nextRevision, err = client.Watch(p.closeCtx, p.keyPrefix, nextRevision, p, clientv3.WithPrefix()); err != nil { - p.logger.Printf("Error processing watch for %s (%s), retry in %s", p.keyPrefix, err, backoff.NextWait()) + log.Printf("Error processing watch for %s (%s), retry in %s", p.keyPrefix, err, backoff.NextWait()) backoff.Wait(p.closeCtx) continue } @@ -185,31 +146,31 @@ func (p *configEtcd) EtcdClientCreated(client etcd.Client) { backoff.Reset() prevRevision = nextRevision } else { - p.logger.Printf("Processing watch for %s interrupted, retry in %s", p.keyPrefix, backoff.NextWait()) + log.Printf("Processing watch for %s interrupted, retry in %s", p.keyPrefix, backoff.NextWait()) backoff.Wait(p.closeCtx) } } - }) + }() } -func (p *configEtcd) EtcdWatchCreated(client etcd.Client, key string) { +func (p *proxyConfigEtcd) EtcdWatchCreated(client *EtcdClient, key string) { } -func (p *configEtcd) getProxyUrls(ctx context.Context, client etcd.Client, keyPrefix string) (*clientv3.GetResponse, error) { +func (p *proxyConfigEtcd) getProxyUrls(ctx context.Context, client *EtcdClient, keyPrefix string) (*clientv3.GetResponse, error) { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() return client.Get(ctx, keyPrefix, clientv3.WithPrefix()) } -func (p *configEtcd) EtcdKeyUpdated(client etcd.Client, key string, data []byte, prevValue []byte) { - var info proxy.InformationEtcd +func (p *proxyConfigEtcd) EtcdKeyUpdated(client *EtcdClient, key string, data []byte, prevValue []byte) { + var info ProxyInformationEtcd if err := json.Unmarshal(data, &info); err != nil { - p.logger.Printf("Could not decode proxy information %s: %s", string(data), err) + log.Printf("Could not decode proxy information %s: %s", string(data), err) return } if err := info.CheckValid(); err != nil { - p.logger.Printf("Received invalid proxy information %s: %s", string(data), err) + log.Printf("Received invalid proxy information %s: %s", string(data), err) return } @@ -224,7 +185,7 @@ func (p *configEtcd) EtcdKeyUpdated(client etcd.Client, key string, data []byte, } if otherKey, otherFound := p.urlToKey[info.Address]; otherFound && otherKey != key { - p.logger.Printf("Address %s is already registered for key %s, ignoring %s", info.Address, otherKey, key) + log.Printf("Address %s is already registered for key %s, ignoring %s", info.Address, otherKey, key) return } @@ -233,25 +194,24 @@ func (p *configEtcd) EtcdKeyUpdated(client etcd.Client, key string, data []byte, p.proxy.KeepConnection(info.Address) } else { if err := p.proxy.AddConnection(false, info.Address); err != nil { - p.logger.Printf("Could not create proxy connection to %s: %s", info.Address, err) + log.Printf("Could not create proxy connection to %s: %s", info.Address, err) return } - p.logger.Printf("Added new connection to %s (from %s)", info.Address, key) + log.Printf("Added new connection to %s (from %s)", info.Address, key) p.keyInfos[key] = &info p.urlToKey[info.Address] = key } } -func (p *configEtcd) EtcdKeyDeleted(client etcd.Client, key string, prevValue []byte) { +func (p *proxyConfigEtcd) EtcdKeyDeleted(client *EtcdClient, key string, prevValue []byte) { p.mu.Lock() defer p.mu.Unlock() p.removeEtcdProxyLocked(key) } -// +checklocks:p.mu -func (p *configEtcd) removeEtcdProxyLocked(key string) { +func (p *proxyConfigEtcd) removeEtcdProxyLocked(key string) { info, found := p.keyInfos[key] if !found { return @@ -260,6 +220,6 @@ func (p *configEtcd) removeEtcdProxyLocked(key string) { delete(p.keyInfos, key) delete(p.urlToKey, info.Address) - p.logger.Printf("Removing connection to %s (from %s)", info.Address, key) + log.Printf("Removing connection to %s (from %s)", info.Address, key) p.proxy.RemoveConnection(info.Address) } diff --git a/sfu/proxy/config_etcd_test.go b/proxy_config_etcd_test.go similarity index 66% rename from sfu/proxy/config_etcd_test.go rename to proxy_config_etcd_test.go index 553fb52..353f690 100644 --- a/sfu/proxy/config_etcd_test.go +++ b/proxy_config_etcd_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package proxy +package signaling import ( "context" @@ -29,9 +29,7 @@ import ( "github.com/dlintw/goconf" "github.com/stretchr/testify/require" - - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" + "go.etcd.io/etcd/server/v3/embed" ) type TestProxyInformationEtcd struct { @@ -40,35 +38,36 @@ type TestProxyInformationEtcd struct { OtherData string `json:"otherdata,omitempty"` } -func newProxyConfigEtcd(t *testing.T, proxy McuProxy) (*etcdtest.Server, Config) { +func newProxyConfigEtcd(t *testing.T, proxy McuProxy) (*embed.Etcd, ProxyConfig) { t.Helper() - embedEtcd, client := etcdtest.NewClientForTest(t) + etcd, client := NewEtcdClientForTest(t) cfg := goconf.NewConfigFile() cfg.AddOption("mcu", "keyprefix", "proxies/") - logger := logtest.NewLoggerForTest(t) - p, err := NewConfigEtcd(logger, cfg, client, proxy) + p, err := NewProxyConfigEtcd(cfg, client, proxy) require.NoError(t, err) t.Cleanup(func() { p.Stop() }) - return embedEtcd, p + return etcd, p } -func SetEtcdProxy(t *testing.T, server *etcdtest.Server, path string, proxy *TestProxyInformationEtcd) { +func SetEtcdProxy(t *testing.T, etcd *embed.Etcd, path string, proxy *TestProxyInformationEtcd) { t.Helper() - data, _ := json.Marshal(proxy) - server.SetValue(path, data) + data, err := json.Marshal(proxy) + require.NoError(t, err) + SetEtcdValue(etcd, path, data) } func TestProxyConfigEtcd(t *testing.T) { t.Parallel() + CatchLogForTest(t) proxy := newMcuProxyForConfig(t) - embedEtcd, config := newProxyConfigEtcd(t, proxy) + etcd, config := newProxyConfigEtcd(t, proxy) - ctx, cancel := context.WithTimeout(t.Context(), time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - SetEtcdProxy(t, embedEtcd, "proxies/a", &TestProxyInformationEtcd{ + SetEtcdProxy(t, etcd, "proxies/a", &TestProxyInformationEtcd{ Address: "https://foo/", }) proxy.Expect("add", "https://foo/") @@ -76,31 +75,31 @@ func TestProxyConfigEtcd(t *testing.T) { proxy.WaitForEvents(ctx) proxy.Expect("add", "https://bar/") - SetEtcdProxy(t, embedEtcd, "proxies/b", &TestProxyInformationEtcd{ + SetEtcdProxy(t, etcd, "proxies/b", &TestProxyInformationEtcd{ Address: "https://bar/", }) proxy.WaitForEvents(ctx) proxy.Expect("keep", "https://bar/") - SetEtcdProxy(t, embedEtcd, "proxies/b", &TestProxyInformationEtcd{ + SetEtcdProxy(t, etcd, "proxies/b", &TestProxyInformationEtcd{ Address: "https://bar/", OtherData: "ignore-me", }) proxy.WaitForEvents(ctx) proxy.Expect("remove", "https://foo/") - embedEtcd.DeleteValue("proxies/a") + DeleteEtcdValue(etcd, "proxies/a") proxy.WaitForEvents(ctx) proxy.Expect("remove", "https://bar/") proxy.Expect("add", "https://baz/") - SetEtcdProxy(t, embedEtcd, "proxies/b", &TestProxyInformationEtcd{ + SetEtcdProxy(t, etcd, "proxies/b", &TestProxyInformationEtcd{ Address: "https://baz/", }) proxy.WaitForEvents(ctx) // Adding the same hostname multiple times should not trigger an event. - SetEtcdProxy(t, embedEtcd, "proxies/c", &TestProxyInformationEtcd{ + SetEtcdProxy(t, etcd, "proxies/c", &TestProxyInformationEtcd{ Address: "https://baz/", }) time.Sleep(100 * time.Millisecond) diff --git a/sfu/proxy/config_static.go b/proxy_config_static.go similarity index 65% rename from sfu/proxy/config_static.go rename to proxy_config_static.go index 1db4ad3..eda67d7 100644 --- a/sfu/proxy/config_static.go +++ b/proxy_config_static.go @@ -19,46 +19,38 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package proxy +package signaling import ( "errors" - "maps" + "log" "net" "net/url" + "strings" "sync" "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) type ipList struct { hostname string - entry *dns.MonitorEntry + entry *DnsMonitorEntry ips []net.IP } -type configStatic struct { - logger log.Logger - mu sync.Mutex - proxy McuProxy +type proxyConfigStatic struct { + mu sync.Mutex + proxy McuProxy - dnsMonitor *dns.Monitor // +checklocksignore: Only written to from constructor. - // +checklocks:mu + dnsMonitor *DnsMonitor dnsDiscovery bool - // +checklocks:mu connectionsMap map[string]*ipList } -func NewConfigStatic(logger log.Logger, config *goconf.ConfigFile, proxy McuProxy, dnsMonitor *dns.Monitor) (Config, error) { - result := &configStatic{ - logger: logger, +func NewProxyConfigStatic(config *goconf.ConfigFile, proxy McuProxy, dnsMonitor *DnsMonitor) (ProxyConfig, error) { + result := &proxyConfigStatic{ proxy: proxy, dnsMonitor: dnsMonitor, connectionsMap: make(map[string]*ipList), @@ -67,16 +59,16 @@ func NewConfigStatic(logger log.Logger, config *goconf.ConfigFile, proxy McuProx return nil, err } if len(result.connectionsMap) == 0 { - return nil, errors.New("no MCU proxy connections configured") + return nil, errors.New("No MCU proxy connections configured") } return result, nil } -func (p *configStatic) configure(cfg *goconf.ConfigFile, fromReload bool) error { +func (p *proxyConfigStatic) configure(config *goconf.ConfigFile, fromReload bool) error { p.mu.Lock() defer p.mu.Unlock() - dnsDiscovery, _ := cfg.GetBool("mcu", "dnsdiscovery") + dnsDiscovery, _ := config.GetBool("mcu", "dnsdiscovery") if dnsDiscovery != p.dnsDiscovery { if !dnsDiscovery { for _, ips := range p.connectionsMap { @@ -89,10 +81,18 @@ func (p *configStatic) configure(cfg *goconf.ConfigFile, fromReload bool) error p.dnsDiscovery = dnsDiscovery } - remove := maps.Clone(p.connectionsMap) + remove := make(map[string]*ipList) + for u, ips := range p.connectionsMap { + remove[u] = ips + } + + mcuUrl, _ := GetStringOptionWithEnv(config, "mcu", "url") + for _, u := range strings.Split(mcuUrl, " ") { + u = strings.TrimSpace(u) + if u == "" { + continue + } - mcuUrl, _ := config.GetStringOptionWithEnv(cfg, "mcu", "url") - for u := range internal.SplitEntries(mcuUrl, " ") { if existing, found := remove[u]; found { // Proxy connection still exists in new configuration delete(remove, u) @@ -106,7 +106,7 @@ func (p *configStatic) configure(cfg *goconf.ConfigFile, fromReload bool) error return err } - p.logger.Printf("Could not parse URL %s: %s", u, err) + log.Printf("Could not parse URL %s: %s", u, err) continue } @@ -115,7 +115,7 @@ func (p *configStatic) configure(cfg *goconf.ConfigFile, fromReload bool) error } if dnsDiscovery { - p.connectionsMap[u] = &ipList{ // +checklocksignore: Not supported for iter loops yet, see https://github.com/google/gvisor/issues/12176 + p.connectionsMap[u] = &ipList{ hostname: parsed.Host, } continue @@ -127,12 +127,12 @@ func (p *configStatic) configure(cfg *goconf.ConfigFile, fromReload bool) error return err } - p.logger.Printf("Could not create proxy connection to %s: %s", u, err) + log.Printf("Could not create proxy connection to %s: %s", u, err) continue } } - p.connectionsMap[u] = &ipList{ // +checklocksignore: Not supported for iter loops yet, see https://github.com/google/gvisor/issues/12176 + p.connectionsMap[u] = &ipList{ hostname: parsed.Host, } } @@ -145,7 +145,7 @@ func (p *configStatic) configure(cfg *goconf.ConfigFile, fromReload bool) error return nil } -func (p *configStatic) Start() error { +func (p *proxyConfigStatic) Start() error { p.mu.Lock() defer p.mu.Unlock() @@ -173,7 +173,7 @@ func (p *configStatic) Start() error { return nil } -func (p *configStatic) Stop() { +func (p *proxyConfigStatic) Stop() { p.mu.Lock() defer p.mu.Unlock() @@ -189,11 +189,11 @@ func (p *configStatic) Stop() { } } -func (p *configStatic) Reload(config *goconf.ConfigFile) error { +func (p *proxyConfigStatic) Reload(config *goconf.ConfigFile) error { return p.configure(config, true) } -func (p *configStatic) onLookup(entry *dns.MonitorEntry, all []net.IP, added []net.IP, keep []net.IP, removed []net.IP) { +func (p *proxyConfigStatic) onLookup(entry *DnsMonitorEntry, all []net.IP, added []net.IP, keep []net.IP, removed []net.IP) { p.mu.Lock() defer p.mu.Unlock() @@ -204,7 +204,7 @@ func (p *configStatic) onLookup(entry *dns.MonitorEntry, all []net.IP, added []n if len(added) > 0 { if err := p.proxy.AddConnection(true, u, added...); err != nil { - p.logger.Printf("Could not add proxy connection to %s with %+v: %s", u, added, err) + log.Printf("Could not add proxy connection to %s with %+v: %s", u, added, err) } } diff --git a/sfu/proxy/config_static_test.go b/proxy_config_static_test.go similarity index 74% rename from sfu/proxy/config_static_test.go rename to proxy_config_static_test.go index 50e5b74..70884e8 100644 --- a/sfu/proxy/config_static_test.go +++ b/proxy_config_static_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package proxy +package signaling import ( "net" @@ -29,21 +29,16 @@ import ( "github.com/dlintw/goconf" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" - dnstest "github.com/strukturag/nextcloud-spreed-signaling/v2/dns/test" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" ) -func newProxyConfigStatic(t *testing.T, proxy McuProxy, dnsDiscovery bool, lookup *dnstest.MockLookup, urls ...string) (Config, *dns.Monitor) { +func newProxyConfigStatic(t *testing.T, proxy McuProxy, dns bool, urls ...string) (ProxyConfig, *DnsMonitor) { cfg := goconf.NewConfigFile() cfg.AddOption("mcu", "url", strings.Join(urls, " ")) - if dnsDiscovery { + if dns { cfg.AddOption("mcu", "dnsdiscovery", "true") } - dnsMonitor := dnstest.NewMonitorForTest(t, time.Hour, lookup) // will be updated manually - logger := logtest.NewLoggerForTest(t) - p, err := NewConfigStatic(logger, cfg, proxy, dnsMonitor) + dnsMonitor := newDnsMonitorForTest(t, time.Hour) // will be updated manually + p, err := NewProxyConfigStatic(cfg, proxy, dnsMonitor) require.NoError(t, err) t.Cleanup(func() { p.Stop() @@ -51,7 +46,7 @@ func newProxyConfigStatic(t *testing.T, proxy McuProxy, dnsDiscovery bool, looku return p, dnsMonitor } -func updateProxyConfigStatic(t *testing.T, config Config, dns bool, urls ...string) { +func updateProxyConfigStatic(t *testing.T, config ProxyConfig, dns bool, urls ...string) { cfg := goconf.NewConfigFile() cfg.AddOption("mcu", "url", strings.Join(urls, " ")) if dns { @@ -61,9 +56,9 @@ func updateProxyConfigStatic(t *testing.T, config Config, dns bool, urls ...stri } func TestProxyConfigStaticSimple(t *testing.T) { - t.Parallel() + CatchLogForTest(t) proxy := newMcuProxyForConfig(t) - config, _ := newProxyConfigStatic(t, proxy, false, nil, "https://foo/") + config, _ := newProxyConfigStatic(t, proxy, false, "https://foo/") proxy.Expect("add", "https://foo/") require.NoError(t, config.Start()) @@ -78,10 +73,10 @@ func TestProxyConfigStaticSimple(t *testing.T) { } func TestProxyConfigStaticDNS(t *testing.T) { - t.Parallel() - lookup := dnstest.NewMockLookup() + CatchLogForTest(t) + lookup := newMockDnsLookupForTest(t) proxy := newMcuProxyForConfig(t) - config, dnsMonitor := newProxyConfigStatic(t, proxy, true, lookup, "https://foo/") + config, dnsMonitor := newProxyConfigStatic(t, proxy, true, "https://foo/") require.NoError(t, config.Start()) time.Sleep(time.Millisecond) @@ -91,7 +86,7 @@ func TestProxyConfigStaticDNS(t *testing.T) { net.ParseIP("10.1.2.3"), }) proxy.Expect("add", "https://foo/", lookup.Get("foo")...) - dnsMonitor.CheckHostnames() + dnsMonitor.checkHostnames() lookup.Set("foo", []net.IP{ net.ParseIP("192.168.0.1"), @@ -101,7 +96,7 @@ func TestProxyConfigStaticDNS(t *testing.T) { proxy.Expect("keep", "https://foo/", net.ParseIP("192.168.0.1")) proxy.Expect("add", "https://foo/", net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.2")) proxy.Expect("remove", "https://foo/", net.ParseIP("10.1.2.3")) - dnsMonitor.CheckHostnames() + dnsMonitor.checkHostnames() proxy.Expect("add", "https://bar/") proxy.Expect("remove", "https://foo/", lookup.Get("foo")...) diff --git a/sfu/proxy/config_test.go b/proxy_config_test.go similarity index 75% rename from sfu/proxy/config_test.go rename to proxy_config_test.go index 887989d..47f51ad 100644 --- a/sfu/proxy/config_test.go +++ b/proxy_config_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package proxy +package signaling import ( "context" @@ -52,12 +52,10 @@ type proxyConfigEvent struct { } type mcuProxyForConfig struct { - t *testing.T - mu sync.Mutex - // +checklocks:mu + t *testing.T expected []proxyConfigEvent - // +checklocks:mu - waiters []chan struct{} + mu sync.Mutex + waiters []chan struct{} } func newMcuProxyForConfig(t *testing.T) *mcuProxyForConfig { @@ -65,8 +63,6 @@ func newMcuProxyForConfig(t *testing.T) *mcuProxyForConfig { t: t, } t.Cleanup(func() { - proxy.mu.Lock() - defer proxy.mu.Unlock() assert.Empty(t, proxy.expected) }) return proxy @@ -87,29 +83,20 @@ func (p *mcuProxyForConfig) Expect(action string, url string, ips ...net.IP) { }) } -func (p *mcuProxyForConfig) addWaiter() chan struct{} { +func (p *mcuProxyForConfig) WaitForEvents(ctx context.Context) { p.t.Helper() p.mu.Lock() defer p.mu.Unlock() if len(p.expected) == 0 { - return nil + return } waiter := make(chan struct{}) p.waiters = append(p.waiters, waiter) - return waiter -} - -func (p *mcuProxyForConfig) WaitForEvents(ctx context.Context) { - p.t.Helper() - - waiter := p.addWaiter() - if waiter == nil { - return - } - + p.mu.Unlock() + defer p.mu.Lock() select { case <-ctx.Done(): assert.NoError(p.t, ctx.Err()) @@ -117,32 +104,6 @@ func (p *mcuProxyForConfig) WaitForEvents(ctx context.Context) { } } -func (p *mcuProxyForConfig) getWaitersIfEmpty() []chan struct{} { - p.mu.Lock() - defer p.mu.Unlock() - - if len(p.expected) != 0 { - return nil - } - - waiters := p.waiters - p.waiters = nil - return waiters -} - -func (p *mcuProxyForConfig) getExpectedEvent() *proxyConfigEvent { - p.mu.Lock() - defer p.mu.Unlock() - - if len(p.expected) == 0 { - return nil - } - - expected := p.expected[0] - p.expected = p.expected[1:] - return &expected -} - func (p *mcuProxyForConfig) checkEvent(event *proxyConfigEvent) { p.t.Helper() pc := make([]uintptr, 32) @@ -160,23 +121,31 @@ func (p *mcuProxyForConfig) checkEvent(event *proxyConfigEvent) { } } - expected := p.getExpectedEvent() - if expected == nil { - assert.Fail(p.t, "no event expected", "received %+v from %s:%d", event, caller.File, caller.Line) + p.mu.Lock() + defer p.mu.Unlock() + + if len(p.expected) == 0 { + assert.Fail(p.t, "no event expected, got %+v from %s:%d", event, caller.File, caller.Line) return } - if !reflect.DeepEqual(expected, event) { - assert.Fail(p.t, "wrong event", "expected %+v, received %+v from %s:%d", expected, event, caller.File, caller.Line) - } + defer func() { + if len(p.expected) == 0 { + waiters := p.waiters + p.waiters = nil + p.mu.Unlock() + defer p.mu.Lock() - waiters := p.getWaitersIfEmpty() - if len(waiters) == 0 { - return - } + for _, ch := range waiters { + ch <- struct{}{} + } + } + }() - for _, ch := range waiters { - ch <- struct{}{} + expected := p.expected[0] + p.expected = p.expected[1:] + if !reflect.DeepEqual(expected, *event) { + assert.Fail(p.t, "expected %+v, got %+v from %s:%d", expected, event, caller.File, caller.Line) } } diff --git a/publisher_stats_counter.go b/publisher_stats_counter.go new file mode 100644 index 0000000..ba8b293 --- /dev/null +++ b/publisher_stats_counter.go @@ -0,0 +1,99 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2021 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "sync" +) + +type publisherStatsCounter struct { + mu sync.Mutex + + streamTypes map[StreamType]bool + subscribers map[string]bool +} + +func (c *publisherStatsCounter) Reset() { + c.mu.Lock() + defer c.mu.Unlock() + + count := len(c.subscribers) + for streamType := range c.streamTypes { + statsMcuPublisherStreamTypesCurrent.WithLabelValues(string(streamType)).Dec() + statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Sub(float64(count)) + } + c.streamTypes = nil + c.subscribers = nil +} + +func (c *publisherStatsCounter) EnableStream(streamType StreamType, enable bool) { + c.mu.Lock() + defer c.mu.Unlock() + + if enable == c.streamTypes[streamType] { + return + } + + if enable { + if c.streamTypes == nil { + c.streamTypes = make(map[StreamType]bool) + } + c.streamTypes[streamType] = true + statsMcuPublisherStreamTypesCurrent.WithLabelValues(string(streamType)).Inc() + statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Add(float64(len(c.subscribers))) + } else { + delete(c.streamTypes, streamType) + statsMcuPublisherStreamTypesCurrent.WithLabelValues(string(streamType)).Dec() + statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Sub(float64(len(c.subscribers))) + } +} + +func (c *publisherStatsCounter) AddSubscriber(id string) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.subscribers[id] { + return + } + + if c.subscribers == nil { + c.subscribers = make(map[string]bool) + } + c.subscribers[id] = true + for streamType := range c.streamTypes { + statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Inc() + } +} + +func (c *publisherStatsCounter) RemoveSubscriber(id string) { + c.mu.Lock() + defer c.mu.Unlock() + + if !c.subscribers[id] { + return + } + + delete(c.subscribers, id) + for streamType := range c.streamTypes { + statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Dec() + } +} diff --git a/publisher_stats_counter_test.go b/publisher_stats_counter_test.go new file mode 100644 index 0000000..975089b --- /dev/null +++ b/publisher_stats_counter_test.go @@ -0,0 +1,111 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2021 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "testing" +) + +func TestPublisherStatsCounter(t *testing.T) { + RegisterJanusMcuStats() + + var c publisherStatsCounter + + c.Reset() + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 0) + c.EnableStream("audio", false) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 0) + c.EnableStream("audio", true) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + c.EnableStream("audio", true) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + c.EnableStream("video", true) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + c.EnableStream("audio", false) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 0) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + c.EnableStream("audio", false) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 0) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + + c.AddSubscriber("1") + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 0) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 0) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 1) + c.EnableStream("audio", true) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 1) + c.AddSubscriber("1") + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 1) + + c.AddSubscriber("2") + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 2) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 2) + + c.RemoveSubscriber("3") + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 2) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 2) + + c.RemoveSubscriber("1") + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 1) + + c.AddSubscriber("1") + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 2) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 2) + + c.EnableStream("audio", false) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 0) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 0) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 2) + + c.EnableStream("audio", true) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 1) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 1) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 2) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 2) + + c.EnableStream("audio", false) + c.EnableStream("video", false) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("audio"), 0) + checkStatsValue(t, statsMcuPublisherStreamTypesCurrent.WithLabelValues("video"), 0) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("audio"), 0) + checkStatsValue(t, statsMcuSubscriberStreamTypesCurrent.WithLabelValues("video"), 0) + + collectAndLint(t, commonMcuStats...) +} diff --git a/server/remotesession.go b/remotesession.go similarity index 60% rename from server/remotesession.go rename to remotesession.go index 9bd17bf..85c271f 100644 --- a/server/remotesession.go +++ b/remotesession.go @@ -19,35 +19,29 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" "encoding/json" "errors" + "fmt" + "log" "sync/atomic" "time" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/client" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) type RemoteSession struct { - logger log.Logger hub *Hub - client *HubClient - remoteClient *grpc.Client - sessionId api.PublicSessionId + client *Client + remoteClient *GrpcClient + sessionId string - proxy atomic.Pointer[grpc.SessionProxy] + proxy atomic.Pointer[SessionProxy] } -func NewRemoteSession(hub *Hub, client *HubClient, remoteClient *grpc.Client, sessionId api.PublicSessionId) (*RemoteSession, error) { +func NewRemoteSession(hub *Hub, client *Client, remoteClient *GrpcClient, sessionId string) (*RemoteSession, error) { remoteSession := &RemoteSession{ - logger: hub.logger, hub: hub, client: client, remoteClient: remoteClient, @@ -68,11 +62,7 @@ func NewRemoteSession(hub *Hub, client *HubClient, remoteClient *grpc.Client, se return remoteSession, nil } -func (s *RemoteSession) GetSessionId() api.PublicSessionId { - return s.sessionId -} - -func (s *RemoteSession) Country() geoip.Country { +func (s *RemoteSession) Country() string { return s.client.Country() } @@ -88,18 +78,18 @@ func (s *RemoteSession) IsConnected() bool { return true } -func (s *RemoteSession) Start(message *api.ClientMessage) error { +func (s *RemoteSession) Start(message *ClientMessage) error { return s.sendMessage(message) } -func (s *RemoteSession) OnProxyMessage(msg *grpc.ServerSessionMessage) error { - var message *api.ServerMessage +func (s *RemoteSession) OnProxyMessage(msg *ServerSessionMessage) error { + var message *ServerMessage if err := json.Unmarshal(msg.Message, &message); err != nil { return err } if !s.client.SendMessage(message) { - return errors.New("could not send message to client") + return fmt.Errorf("could not send message to client") } return nil @@ -107,12 +97,12 @@ func (s *RemoteSession) OnProxyMessage(msg *grpc.ServerSessionMessage) error { func (s *RemoteSession) OnProxyClose(err error) { if err != nil { - s.logger.Printf("Proxy connection for session %s to %s was closed with error: %s", s.sessionId, s.remoteClient.Target(), err) + log.Printf("Proxy connection for session %s to %s was closed with error: %s", s.sessionId, s.remoteClient.Target(), err) } s.Close() } -func (s *RemoteSession) SendMessage(message client.WritableClientMessage) bool { +func (s *RemoteSession) SendMessage(message WritableClientMessage) bool { return s.sendMessage(message) == nil } @@ -122,13 +112,13 @@ func (s *RemoteSession) sendProxyMessage(message []byte) error { return errors.New("proxy already closed") } - msg := &grpc.ClientSessionMessage{ + msg := &ClientSessionMessage{ Message: message, } return proxy.Send(msg) } -func (s *RemoteSession) sendMessage(message any) error { +func (s *RemoteSession) sendMessage(message interface{}) error { data, err := json.Marshal(message) if err != nil { return err @@ -145,25 +135,20 @@ func (s *RemoteSession) Close() { s.client.Close() } -func (s *RemoteSession) OnLookupCountry(addr string) geoip.Country { - return s.hub.LookupCountry(addr) +func (s *RemoteSession) OnLookupCountry(client HandlerClient) string { + return s.hub.OnLookupCountry(client) } -func (s *RemoteSession) OnClosed() { +func (s *RemoteSession) OnClosed(client HandlerClient) { s.Close() } -func (s *RemoteSession) OnMessageReceived(message []byte) { +func (s *RemoteSession) OnMessageReceived(client HandlerClient, message []byte) { if err := s.sendProxyMessage(message); err != nil { - s.logger.Printf("Error sending %s to the proxy for session %s: %s", string(message), s.sessionId, err) + log.Printf("Error sending %s to the proxy for session %s: %s", string(message), s.sessionId, err) s.Close() } } -func (s *RemoteSession) OnRTTReceived(rtt time.Duration) { - // Ignore -} - -func (s *RemoteSession) IsInRoom(id string) bool { - return s.client.IsInRoom(id) +func (s *RemoteSession) OnRTTReceived(client HandlerClient, rtt time.Duration) { } diff --git a/server/room.go b/room.go similarity index 53% rename from server/room.go rename to room.go index d4179c2..e6e04fb 100644 --- a/server/room.go +++ b/room.go @@ -19,29 +19,20 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "bytes" "context" "encoding/json" - "errors" "fmt" - "maps" + "log" "net/url" "strconv" "sync" "time" "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) const ( @@ -70,47 +61,33 @@ func init() { type Room struct { id string - logger log.Logger hub *Hub - events events.AsyncEvents - backend *talk.Backend + events AsyncEvents + backend *Backend - // +checklocks:mu properties json.RawMessage - closer *internal.Closer - mu *sync.RWMutex - asyncCh events.AsyncChannel - // +checklocks:mu - sessions map[api.PublicSessionId]Session - // +checklocks:mu - internalSessions map[*ClientSession]bool - // +checklocks:mu - virtualSessions map[*VirtualSession]bool - // +checklocks:mu - inCallSessions map[Session]bool - // +checklocks:mu - roomSessionData map[api.PublicSessionId]*talk.RoomSessionData + closer *Closer + mu *sync.RWMutex + sessions map[string]Session + + internalSessions map[*ClientSession]bool + virtualSessions map[*VirtualSession]bool + inCallSessions map[Session]bool + roomSessionData map[string]*RoomSessionData - // +checklocks:mu statsRoomSessionsCurrent *prometheus.GaugeVec - // +checklocks:mu - statsCallSessionsCurrent *prometheus.GaugeVec - // +checklocks:mu - statsCallSessionsTotal *prometheus.CounterVec - // +checklocks:mu - statsCallRoomsTotal prometheus.Counter // Users currently in the room - users []api.StringMap + users []map[string]interface{} // Timestamps of last backend requests for the different types. lastRoomRequests map[string]int64 - transientData *api.TransientData + transientData *TransientData } -func getRoomIdForBackend(id string, backend *talk.Backend) string { +func getRoomIdForBackend(id string, backend *Backend) string { if id == "" { return "" } @@ -118,45 +95,35 @@ func getRoomIdForBackend(id string, backend *talk.Backend) string { return backend.Id() + "|" + id } -func NewRoom(roomId string, properties json.RawMessage, hub *Hub, asyncEvents events.AsyncEvents, backend *talk.Backend) (*Room, error) { +func NewRoom(roomId string, properties json.RawMessage, hub *Hub, events AsyncEvents, backend *Backend) (*Room, error) { room := &Room{ id: roomId, - logger: hub.logger, hub: hub, - events: asyncEvents, + events: events, backend: backend, properties: properties, - closer: internal.NewCloser(), + closer: NewCloser(), mu: &sync.RWMutex{}, - asyncCh: make(events.AsyncChannel, events.DefaultAsyncChannelSize), - sessions: make(map[api.PublicSessionId]Session), + sessions: make(map[string]Session), internalSessions: make(map[*ClientSession]bool), virtualSessions: make(map[*VirtualSession]bool), inCallSessions: make(map[Session]bool), - roomSessionData: make(map[api.PublicSessionId]*talk.RoomSessionData), + roomSessionData: make(map[string]*RoomSessionData), statsRoomSessionsCurrent: statsRoomSessionsCurrent.MustCurryWith(prometheus.Labels{ "backend": backend.Id(), "room": roomId, }), - statsCallSessionsCurrent: statsCallSessionsCurrent.MustCurryWith(prometheus.Labels{ - "backend": backend.Id(), - "room": roomId, - }), - statsCallSessionsTotal: statsCallSessionsTotal.MustCurryWith(prometheus.Labels{ - "backend": backend.Id(), - }), - statsCallRoomsTotal: statsCallRoomsTotal.WithLabelValues(backend.Id()), lastRoomRequests: make(map[string]int64), - transientData: api.NewTransientData(), + transientData: NewTransientData(), } - if err := asyncEvents.RegisterBackendRoomListener(roomId, backend, room); err != nil { + if err := events.RegisterBackendRoomListener(roomId, backend, room); err != nil { return nil, err } @@ -175,7 +142,7 @@ func (r *Room) Properties() json.RawMessage { return r.properties } -func (r *Room) Backend() *talk.Backend { +func (r *Room) Backend() *Backend { return r.backend } @@ -201,10 +168,6 @@ func (r *Room) IsEqual(other *Room) bool { return b1.Id() == b2.Id() } -func (r *Room) AsyncChannel() events.AsyncChannel { - return r.asyncCh -} - func (r *Room) run() { ticker := time.NewTicker(updateActiveSessionsInterval) loop: @@ -212,11 +175,6 @@ loop: select { case <-r.closer.C: break loop - case msg := <-r.asyncCh: - r.processAsyncNatsMessage(msg) - for count := len(r.asyncCh); count > 0; count-- { - r.processAsyncNatsMessage(<-r.asyncCh) - } case <-ticker.C: r.publishActiveSessions() } @@ -228,65 +186,50 @@ func (r *Room) doClose() { } func (r *Room) unsubscribeBackend() { - if err := r.events.UnregisterBackendRoomListener(r.id, r.backend, r); err != nil && !errors.Is(err, nats.ErrConnectionClosed) { - r.logger.Printf("Error unsubscribing room listener in %s: %s", r.id, err) - } + r.events.UnregisterBackendRoomListener(r.id, r.backend, r) } func (r *Room) Close() []Session { r.hub.removeRoom(r) r.doClose() r.mu.Lock() - defer r.mu.Unlock() r.unsubscribeBackend() result := make([]Session, 0, len(r.sessions)) for _, s := range r.sessions { result = append(result, s) } r.sessions = nil - r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeClient)}) - r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeFederation)}) - r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeInternal)}) - r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeVirtual)}) - r.clearInCallStats() + r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": HelloClientTypeClient}) + r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": HelloClientTypeInternal}) + r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": HelloClientTypeVirtual}) + r.mu.Unlock() return result } -func (r *Room) processAsyncNatsMessage(msg *nats.Msg) { - var message events.AsyncMessage - if err := nats.Decode(msg, &message); err != nil { - r.logger.Printf("Could not decode NATS message %+v: %s", msg, err) - return - } - - r.processAsyncMessage(&message) -} - -func (r *Room) processAsyncMessage(message *events.AsyncMessage) { +func (r *Room) ProcessBackendRoomRequest(message *AsyncMessage) { switch message.Type { case "room": r.processBackendRoomRequestRoom(message.Room) case "asyncroom": r.processBackendRoomRequestAsyncRoom(message.AsyncRoom) default: - r.logger.Printf("Unsupported backend room request with type %s in %s: %+v", message.Type, r.id, message) + log.Printf("Unsupported backend room request with type %s in %s: %+v", message.Type, r.id, message) } } -func (r *Room) processBackendRoomRequestRoom(message *talk.BackendServerRoomRequest) { +func (r *Room) processBackendRoomRequestRoom(message *BackendServerRoomRequest) { received := message.ReceivedTime if last, found := r.lastRoomRequests[message.Type]; found && last > received { if msg, err := json.Marshal(message); err == nil { - r.logger.Printf("Ignore old backend room request for %s: %s", r.Id(), string(msg)) + log.Printf("Ignore old backend room request for %s: %s", r.Id(), string(msg)) } else { - r.logger.Printf("Ignore old backend room request for %s: %+v", r.Id(), message) + log.Printf("Ignore old backend room request for %s: %+v", r.Id(), message) } return } r.lastRoomRequests[message.Type] = received - message.RoomId = r.Id() - message.Backend = r.Backend() + message.room = r switch message.Type { case "update": r.hub.roomUpdated <- message @@ -303,55 +246,50 @@ func (r *Room) processBackendRoomRequestRoom(message *talk.BackendServerRoomRequ r.publishSwitchTo(message.SwitchTo) case "transient": switch message.Transient.Action { - case talk.TransientActionSet: - if message.Transient.TTL == 0 { - r.doSetTransientData(message.Transient.Key, message.Transient.Value) - } else { - r.doSetTransientDataTTL(message.Transient.Key, message.Transient.Value, message.Transient.TTL) - } - case talk.TransientActionDelete: - r.doRemoveTransientData(message.Transient.Key) + case TransientActionSet: + r.SetTransientDataTTL(message.Transient.Key, message.Transient.Value, message.Transient.TTL) + case TransientActionDelete: + r.RemoveTransientData(message.Transient.Key) default: - r.logger.Printf("Unsupported transient action in room %s: %+v", r.Id(), message.Transient) + log.Printf("Unsupported transient action in room %s: %+v", r.Id(), message.Transient) } default: - r.logger.Printf("Unsupported backend room request with type %s in %s: %+v", message.Type, r.Id(), message) + log.Printf("Unsupported backend room request with type %s in %s: %+v", message.Type, r.Id(), message) } } -func (r *Room) processBackendRoomRequestAsyncRoom(message *events.AsyncRoomMessage) { +func (r *Room) processBackendRoomRequestAsyncRoom(message *AsyncRoomMessage) { switch message.Type { case "sessionjoined": r.notifySessionJoined(message.SessionId) - if message.ClientType == api.HelloClientTypeInternal { + if message.ClientType == HelloClientTypeInternal { r.publishUsersChangedWithInternal() } default: - r.logger.Printf("Unsupported async room request with type %s in %s: %+v", message.Type, r.Id(), message) + log.Printf("Unsupported async room request with type %s in %s: %+v", message.Type, r.Id(), message) } } func (r *Room) AddSession(session Session, sessionData json.RawMessage) { - var roomSessionData *talk.RoomSessionData + var roomSessionData *RoomSessionData if len(sessionData) > 0 { - roomSessionData = &talk.RoomSessionData{} + roomSessionData = &RoomSessionData{} if err := json.Unmarshal(sessionData, roomSessionData); err != nil { - r.logger.Printf("Error decoding room session data \"%s\": %s", string(sessionData), err) + log.Printf("Error decoding room session data \"%s\": %s", string(sessionData), err) roomSessionData = nil } } sid := session.PublicId() r.mu.Lock() - isFirst := len(r.sessions) == 0 _, found := r.sessions[sid] r.sessions[sid] = session if !found { - r.statsRoomSessionsCurrent.With(prometheus.Labels{"clienttype": string(session.ClientType())}).Inc() + r.statsRoomSessionsCurrent.With(prometheus.Labels{"clienttype": session.ClientType()}).Inc() } var publishUsersChanged bool switch session.ClientType() { - case api.HelloClientTypeInternal: + case HelloClientTypeInternal: clientSession, ok := session.(*ClientSession) if !ok { delete(r.sessions, sid) @@ -359,7 +297,7 @@ func (r *Room) AddSession(session Session, sessionData json.RawMessage) { panic(fmt.Sprintf("Expected a client session, got %v (%T)", session, session)) } r.internalSessions[clientSession] = true - case api.HelloClientTypeVirtual: + case HelloClientTypeVirtual: virtualSession, ok := session.(*VirtualSession) if !ok { delete(r.sessions, sid) @@ -371,7 +309,7 @@ func (r *Room) AddSession(session Session, sessionData json.RawMessage) { } if roomSessionData != nil { r.roomSessionData[sid] = roomSessionData - r.logger.Printf("Session %s sent room session data %+v", session.PublicId(), roomSessionData) + log.Printf("Session %s sent room session data %+v", session.PublicId(), roomSessionData) } r.mu.Unlock() if !found { @@ -385,25 +323,22 @@ func (r *Room) AddSession(session Session, sessionData json.RawMessage) { if clientSession, ok := session.(*ClientSession); ok { r.transientData.AddListener(clientSession) } - if isFirst { - r.fetchInitialTransientData() - } } // Trigger notifications that the session joined. - if err := r.events.PublishBackendRoomMessage(r.id, r.backend, &events.AsyncMessage{ + if err := r.events.PublishBackendRoomMessage(r.id, r.backend, &AsyncMessage{ Type: "asyncroom", - AsyncRoom: &events.AsyncRoomMessage{ + AsyncRoom: &AsyncRoomMessage{ Type: "sessionjoined", SessionId: sid, ClientType: session.ClientType(), }, }); err != nil { - r.logger.Printf("Error publishing joined event for session %s: %s", sid, err) + log.Printf("Error publishing joined event for session %s: %s", sid, err) } } -func (r *Room) getOtherSessions(ignoreSessionId api.PublicSessionId) (Session, []Session) { +func (r *Room) getOtherSessions(ignoreSessionId string) (Session, []Session) { r.mu.Lock() defer r.mu.Unlock() @@ -419,19 +354,19 @@ func (r *Room) getOtherSessions(ignoreSessionId api.PublicSessionId) (Session, [ return r.sessions[ignoreSessionId], sessions } -func (r *Room) notifySessionJoined(sessionId api.PublicSessionId) { +func (r *Room) notifySessionJoined(sessionId string) { session, sessions := r.getOtherSessions(sessionId) if len(sessions) == 0 { return } - if session != nil && session.ClientType() != api.HelloClientTypeClient { + if session != nil && session.ClientType() != HelloClientTypeClient { session = nil } - joinEvents := make([]api.EventServerMessageSessionEntry, 0, len(sessions)) + events := make([]*EventServerMessageSessionEntry, 0, len(sessions)) for _, s := range sessions { - entry := api.EventServerMessageSessionEntry{ + entry := &EventServerMessageSessionEntry{ SessionId: s.PublicId(), UserId: s.UserId(), User: s.UserData(), @@ -439,25 +374,25 @@ func (r *Room) notifySessionJoined(sessionId api.PublicSessionId) { if s, ok := s.(*ClientSession); ok { entry.Features = s.GetFeatures() entry.RoomSessionId = s.RoomSessionId() - entry.Federated = s.ClientType() == api.HelloClientTypeFederation + entry.Federated = s.ClientType() == HelloClientTypeFederation } - joinEvents = append(joinEvents, entry) + events = append(events, entry) } - msg := &api.ServerMessage{ + msg := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "join", - Join: joinEvents, + Join: events, }, } - if err := r.events.PublishSessionMessage(sessionId, r.backend, &events.AsyncMessage{ + if err := r.events.PublishSessionMessage(sessionId, r.backend, &AsyncMessage{ Type: "message", Message: msg, }); err != nil { - r.logger.Printf("Error publishing joined events to session %s: %s", sessionId, err) + log.Printf("Error publishing joined events to session %s: %s", sessionId, err) } // Notify about initial flags of virtual sessions. @@ -472,12 +407,12 @@ func (r *Room) notifySessionJoined(sessionId api.PublicSessionId) { continue } - msg := &api.ServerMessage{ + msg := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "participants", Type: "flags", - Flags: &api.RoomFlagsServerMessage{ + Flags: &RoomFlagsServerMessage{ RoomId: r.id, SessionId: vsess.PublicId(), Flags: vsess.Flags(), @@ -485,80 +420,27 @@ func (r *Room) notifySessionJoined(sessionId api.PublicSessionId) { }, } - if err := r.events.PublishSessionMessage(sessionId, r.backend, &events.AsyncMessage{ + if err := r.events.PublishSessionMessage(sessionId, r.backend, &AsyncMessage{ Type: "message", Message: msg, }); err != nil { - r.logger.Printf("Error publishing initial flags to session %s: %s", sessionId, err) + log.Printf("Error publishing initial flags to session %s: %s", sessionId, err) } } } func (r *Room) HasSession(session Session) bool { r.mu.RLock() - defer r.mu.RUnlock() - _, result := r.sessions[session.PublicId()] + r.mu.RUnlock() return result } func (r *Room) IsSessionInCall(session Session) bool { r.mu.RLock() - defer r.mu.RUnlock() - - return r.inCallSessions[session] -} - -// +checklocks:r.mu -func (r *Room) clearInCallStats() { - r.statsCallSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeClient)}) - r.statsCallSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeFederation)}) - r.statsCallSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeInternal)}) - r.statsCallSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeVirtual)}) -} - -func (r *Room) addSessionToCall(session Session) bool { - r.mu.Lock() - defer r.mu.Unlock() - - return r.addSessionToCallLocked(session) -} - -// +checklocks:r.mu -func (r *Room) addSessionToCallLocked(session Session) bool { - if r.inCallSessions[session] { - return false - } - - if len(r.inCallSessions) == 0 { - r.statsCallRoomsTotal.Inc() - } - r.inCallSessions[session] = true - r.statsCallSessionsCurrent.WithLabelValues(string(session.ClientType())).Inc() - r.statsCallSessionsTotal.WithLabelValues(string(session.ClientType())).Inc() - return true -} - -func (r *Room) removeSessionFromCall(session Session) bool { - r.mu.Lock() - defer r.mu.Unlock() - - return r.removeSessionFromCallLocked(session) -} - -// +checklocks:r.mu -func (r *Room) removeSessionFromCallLocked(session Session) bool { - if !r.inCallSessions[session] { - return false - } - - delete(r.inCallSessions, session) - if len(r.inCallSessions) == 0 { - r.clearInCallStats() - } else { - r.statsCallSessionsCurrent.WithLabelValues(string(session.ClientType())).Dec() - } - return true + _, result := r.inCallSessions[session] + r.mu.RUnlock() + return result } // Returns "true" if there are still clients in the room. @@ -570,14 +452,14 @@ func (r *Room) RemoveSession(session Session) bool { } sid := session.PublicId() - r.statsRoomSessionsCurrent.With(prometheus.Labels{"clienttype": string(session.ClientType())}).Dec() + r.statsRoomSessionsCurrent.With(prometheus.Labels{"clienttype": session.ClientType()}).Dec() delete(r.sessions, sid) if virtualSession, ok := session.(*VirtualSession); ok { delete(r.virtualSessions, virtualSession) // Handle case where virtual session was also sent by Nextcloud. - users := make([]api.StringMap, 0, len(r.users)) + users := make([]map[string]interface{}, 0, len(r.users)) for _, u := range r.users { - if value, found := api.GetStringMapString[api.PublicSessionId](u, "sessionId"); !found || value != sid { + if u["sessionId"] != sid { users = append(users, u) } } @@ -589,34 +471,28 @@ func (r *Room) RemoveSession(session Session) bool { delete(r.internalSessions, clientSession) r.transientData.RemoveListener(clientSession) } - r.removeSessionFromCallLocked(session) + delete(r.inCallSessions, session) delete(r.roomSessionData, sid) if len(r.sessions) > 0 { r.mu.Unlock() - if err := r.RemoveTransientData(api.TransientSessionDataPrefix + string(sid)); err != nil { - r.logger.Printf("Error removing transient data for session %s", sid) - } r.PublishSessionLeft(session) return true } r.hub.removeRoom(r) - r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeClient)}) - r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeInternal)}) - r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": string(api.HelloClientTypeVirtual)}) + r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": HelloClientTypeClient}) + r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": HelloClientTypeInternal}) + r.statsRoomSessionsCurrent.Delete(prometheus.Labels{"clienttype": HelloClientTypeVirtual}) r.unsubscribeBackend() r.doClose() r.mu.Unlock() - if err := r.RemoveTransientData(api.TransientSessionDataPrefix + string(sid)); err != nil { - r.logger.Printf("Error removing transient data for session %s", sid) - } // Still need to publish an event so sessions on other servers get notified. r.PublishSessionLeft(session) return false } -func (r *Room) publish(message *api.ServerMessage) error { - return r.events.PublishRoomMessage(r.id, r.backend, &events.AsyncMessage{ +func (r *Room) publish(message *ServerMessage) error { + return r.events.PublishRoomMessage(r.id, r.backend, &AsyncMessage{ Type: "message", Message: message, }) @@ -632,25 +508,25 @@ func (r *Room) UpdateProperties(properties json.RawMessage) { } r.properties = properties - message := &api.ServerMessage{ + message := &ServerMessage{ Type: "room", - Room: &api.RoomServerMessage{ + Room: &RoomServerMessage{ RoomId: r.id, Properties: r.properties, }, } if err := r.publish(message); err != nil { - r.logger.Printf("Could not publish update properties message in room %s: %s", r.Id(), err) + log.Printf("Could not publish update properties message in room %s: %s", r.Id(), err) } } -func (r *Room) GetRoomSessionData(session Session) *talk.RoomSessionData { +func (r *Room) GetRoomSessionData(session Session) *RoomSessionData { r.mu.RLock() defer r.mu.RUnlock() return r.roomSessionData[session.PublicId()] } -func (r *Room) PublishSessionJoined(session Session, sessionData *talk.RoomSessionData) { +func (r *Room) PublishSessionJoined(session Session, sessionData *RoomSessionData) { sessionId := session.PublicId() if sessionId == "" { return @@ -661,12 +537,12 @@ func (r *Room) PublishSessionJoined(session Session, sessionData *talk.RoomSessi userid = sessionData.UserId } - message := &api.ServerMessage{ + message := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "join", - Join: []api.EventServerMessageSessionEntry{ + Join: []*EventServerMessageSessionEntry{ { SessionId: sessionId, UserId: userid, @@ -678,10 +554,10 @@ func (r *Room) PublishSessionJoined(session Session, sessionData *talk.RoomSessi if session, ok := session.(*ClientSession); ok { message.Event.Join[0].Features = session.GetFeatures() message.Event.Join[0].RoomSessionId = session.RoomSessionId() - message.Event.Join[0].Federated = session.ClientType() == api.HelloClientTypeFederation + message.Event.Join[0].Federated = session.ClientType() == HelloClientTypeFederation } if err := r.publish(message); err != nil { - r.logger.Printf("Could not publish session joined message in room %s: %s", r.Id(), err) + log.Printf("Could not publish session joined message in room %s: %s", r.Id(), err) } } @@ -691,65 +567,70 @@ func (r *Room) PublishSessionLeft(session Session) { return } - message := &api.ServerMessage{ + message := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "leave", - Leave: []api.PublicSessionId{ + Leave: []string{ sessionId, }, }, } if err := r.publish(message); err != nil { - r.logger.Printf("Could not publish session left message in room %s: %s", r.Id(), err) + log.Printf("Could not publish session left message in room %s: %s", r.Id(), err) } - if session.ClientType() == api.HelloClientTypeInternal { + if session.ClientType() == HelloClientTypeInternal { r.publishUsersChangedWithInternal() } } -// +checklocksread:r.mu -func (r *Room) getClusteredInternalSessionsRLocked() (internal map[api.PublicSessionId]*grpc.InternalSessionData, virtual map[api.PublicSessionId]*grpc.VirtualSessionData) { +func (r *Room) getClusteredInternalSessionsRLocked() (internal map[string]*InternalSessionData, virtual map[string]*VirtualSessionData) { if r.hub.rpcClients == nil { return nil, nil } r.mu.RUnlock() defer r.mu.RLock() - ctx := log.NewLoggerContext(context.Background(), r.logger) - ctx, cancel := context.WithTimeout(ctx, time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() var mu sync.Mutex var wg sync.WaitGroup for _, client := range r.hub.rpcClients.GetClients() { - wg.Go(func() { - clientInternal, clientVirtual, err := client.GetInternalSessions(ctx, r.Id(), r.Backend().Urls()) + wg.Add(1) + go func(c *GrpcClient) { + defer wg.Done() + + clientInternal, clientVirtual, err := c.GetInternalSessions(ctx, r.Id(), r.Backend()) if err != nil { - r.logger.Printf("Received error while getting internal sessions for %s@%s from %s: %s", r.Id(), r.Backend().Id(), client.Target(), err) + log.Printf("Received error while getting internal sessions for %s@%s from %s: %s", r.Id(), r.Backend().Id(), c.Target(), err) return } mu.Lock() defer mu.Unlock() if internal == nil { - internal = make(map[api.PublicSessionId]*grpc.InternalSessionData, len(clientInternal)) + internal = make(map[string]*InternalSessionData, len(clientInternal)) + } + for sid, s := range clientInternal { + internal[sid] = s } - maps.Copy(internal, clientInternal) if virtual == nil { - virtual = make(map[api.PublicSessionId]*grpc.VirtualSessionData, len(clientVirtual)) + virtual = make(map[string]*VirtualSessionData, len(clientVirtual)) } - maps.Copy(virtual, clientVirtual) - }) + for sid, s := range clientVirtual { + virtual[sid] = s + } + }(client) } wg.Wait() return } -func (r *Room) addInternalSessions(users []api.StringMap) []api.StringMap { +func (r *Room) addInternalSessions(users []map[string]interface{}) []map[string]interface{} { now := time.Now().Unix() r.mu.RLock() defer r.mu.RUnlock() @@ -764,34 +645,36 @@ func (r *Room) addInternalSessions(users []api.StringMap) []api.StringMap { return users } - skipSession := make(map[api.PublicSessionId]bool) + skipSession := make(map[string]bool) for _, user := range users { - sessionid, found := api.GetStringMapString[api.PublicSessionId](user, "sessionId") + sessionid, found := user["sessionId"] if !found || sessionid == "" { continue } if userid, found := user["userId"]; !found || userid == "" { - if roomSessionData, found := r.roomSessionData[sessionid]; found { + if roomSessionData, found := r.roomSessionData[sessionid.(string)]; found { user["userId"] = roomSessionData.UserId - } else if entry, found := clusteredVirtualSessions[sessionid]; found { - user["virtual"] = true - user["inCall"] = entry.GetInCall() - skipSession[sessionid] = true - } else { - for session := range r.virtualSessions { - if session.PublicId() == sessionid { - user["virtual"] = true - user["inCall"] = session.GetInCall() - skipSession[sessionid] = true - break + } else if sid, ok := sessionid.(string); ok { + if entry, found := clusteredVirtualSessions[sid]; found { + user["virtual"] = true + user["inCall"] = entry.GetInCall() + skipSession[sid] = true + } else { + for session := range r.virtualSessions { + if session.PublicId() == sid { + user["virtual"] = true + user["inCall"] = session.GetInCall() + skipSession[sid] = true + break + } } } } } } for session := range r.internalSessions { - u := api.StringMap{ + u := map[string]interface{}{ "inCall": session.GetInCall(), "sessionId": session.PublicId(), "lastPing": now, @@ -803,7 +686,7 @@ func (r *Room) addInternalSessions(users []api.StringMap) []api.StringMap { users = append(users, u) } for _, session := range clusteredInternalSessions { - u := api.StringMap{ + u := map[string]interface{}{ "inCall": session.GetInCall(), "sessionId": session.GetSessionId(), "lastPing": now, @@ -820,7 +703,7 @@ func (r *Room) addInternalSessions(users []api.StringMap) []api.StringMap { continue } skipSession[sid] = true - users = append(users, api.StringMap{ + users = append(users, map[string]interface{}{ "inCall": session.GetInCall(), "sessionId": sid, "lastPing": now, @@ -832,7 +715,7 @@ func (r *Room) addInternalSessions(users []api.StringMap) []api.StringMap { continue } - users = append(users, api.StringMap{ + users = append(users, map[string]interface{}{ "inCall": session.GetInCall(), "sessionId": sid, "lastPing": now, @@ -842,14 +725,14 @@ func (r *Room) addInternalSessions(users []api.StringMap) []api.StringMap { return users } -func (r *Room) filterPermissions(users []api.StringMap) []api.StringMap { +func (r *Room) filterPermissions(users []map[string]interface{}) []map[string]interface{} { for _, user := range users { delete(user, "permissions") } return users } -func IsInCall(value any) (bool, bool) { +func IsInCall(value interface{}) (bool, bool) { switch value := value.(type) { case bool: return value, true @@ -869,7 +752,7 @@ func IsInCall(value any) (bool, bool) { } } -func (r *Room) PublishUsersInCallChanged(changed []api.StringMap, users []api.StringMap) { +func (r *Room) PublishUsersInCallChanged(changed []map[string]interface{}, users []map[string]interface{}) { r.users = users for _, user := range changed { inCallInterface, found := user["inCall"] @@ -881,25 +764,35 @@ func (r *Room) PublishUsersInCallChanged(changed []api.StringMap, users []api.St continue } - sessionId, found := api.GetStringMapString[api.PublicSessionId](user, "sessionId") + sessionIdInterface, found := user["sessionId"] if !found { - sessionId, found = api.GetStringMapString[api.PublicSessionId](user, "sessionid") + sessionIdInterface, found = user["sessionid"] if !found { continue } } + sessionId, ok := sessionIdInterface.(string) + if !ok { + continue + } + session := r.hub.GetSessionByPublicId(sessionId) if session == nil { continue } if inCall { - if r.addSessionToCall(session) { - r.logger.Printf("Session %s joined call %s", session.PublicId(), r.id) + r.mu.Lock() + if !r.inCallSessions[session] { + r.inCallSessions[session] = true + log.Printf("Session %s joined call %s", session.PublicId(), r.id) } + r.mu.Unlock() } else { - r.removeSessionFromCall(session) + r.mu.Lock() + delete(r.inCallSessions, session) + r.mu.Unlock() if clientSession, ok := session.(*ClientSession); ok { clientSession.LeaveCall() } @@ -909,12 +802,12 @@ func (r *Room) PublishUsersInCallChanged(changed []api.StringMap, users []api.St changed = r.filterPermissions(changed) users = r.filterPermissions(users) - message := &api.ServerMessage{ + message := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "participants", Type: "update", - Update: &api.RoomEventServerMessage{ + Update: &RoomEventServerMessage{ RoomId: r.id, Changed: changed, Users: r.addInternalSessions(users), @@ -922,7 +815,7 @@ func (r *Room) PublishUsersInCallChanged(changed []api.StringMap, users []api.St }, } if err := r.publish(message); err != nil { - r.logger.Printf("Could not publish incall message in room %s: %s", r.Id(), err) + log.Printf("Could not publish incall message in room %s: %s", r.Id(), err) } } @@ -933,19 +826,20 @@ func (r *Room) PublishUsersInCallChangedAll(inCall int) { var notify []*ClientSession if inCall&FlagInCall != 0 { // All connected sessions join the call. - var joined []api.PublicSessionId + var joined []string for _, session := range r.sessions { clientSession, ok := session.(*ClientSession) if !ok { continue } - if session.ClientType() == api.HelloClientTypeInternal || - session.ClientType() == api.HelloClientTypeFederation { + if session.ClientType() == HelloClientTypeInternal || + session.ClientType() == HelloClientTypeFederation { continue } - if r.addSessionToCallLocked(session) { + if !r.inCallSessions[session] { + r.inCallSessions[session] = true joined = append(joined, session.PublicId()) } notify = append(notify, clientSession) @@ -955,7 +849,7 @@ func (r *Room) PublishUsersInCallChangedAll(inCall int) { return } - r.logger.Printf("Sessions %v joined call %s", joined, r.id) + log.Printf("Sessions %v joined call %s", joined, r.id) } else if len(r.inCallSessions) > 0 { // Perform actual leaving asynchronously. ch := make(chan *ClientSession, 1) @@ -985,8 +879,7 @@ func (r *Room) PublishUsersInCallChangedAll(inCall int) { } } close(ch) - clear(r.inCallSessions) - r.clearInCallStats() + r.inCallSessions = make(map[Session]bool) } else { // All sessions already left the call, no need to notify. return @@ -994,12 +887,12 @@ func (r *Room) PublishUsersInCallChangedAll(inCall int) { inCallMsg := json.RawMessage(strconv.FormatInt(int64(inCall), 10)) - message := &api.ServerMessage{ + message := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "participants", Type: "update", - Update: &api.RoomEventServerMessage{ + Update: &RoomEventServerMessage{ RoomId: r.id, InCall: inCallMsg, All: true, @@ -1009,21 +902,21 @@ func (r *Room) PublishUsersInCallChangedAll(inCall int) { for _, session := range notify { if !session.SendMessage(message) { - r.logger.Printf("Could not send incall message from room %s to %s", r.Id(), session.PublicId()) + log.Printf("Could not send incall message from room %s to %s", r.Id(), session.PublicId()) } } } -func (r *Room) PublishUsersChanged(changed []api.StringMap, users []api.StringMap) { +func (r *Room) PublishUsersChanged(changed []map[string]interface{}, users []map[string]interface{}) { changed = r.filterPermissions(changed) users = r.filterPermissions(users) - message := &api.ServerMessage{ + message := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "participants", Type: "update", - Update: &api.RoomEventServerMessage{ + Update: &RoomEventServerMessage{ RoomId: r.id, Changed: changed, Users: r.addInternalSessions(users), @@ -1031,19 +924,19 @@ func (r *Room) PublishUsersChanged(changed []api.StringMap, users []api.StringMa }, } if err := r.publish(message); err != nil { - r.logger.Printf("Could not publish users changed message in room %s: %s", r.Id(), err) + log.Printf("Could not publish users changed message in room %s: %s", r.Id(), err) } } -func (r *Room) getParticipantsUpdateMessage(users []api.StringMap) *api.ServerMessage { +func (r *Room) getParticipantsUpdateMessage(users []map[string]interface{}) *ServerMessage { users = r.filterPermissions(users) - message := &api.ServerMessage{ + message := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "participants", Type: "update", - Update: &api.RoomEventServerMessage{ + Update: &RoomEventServerMessage{ RoomId: r.id, Users: r.addInternalSessions(users), }, @@ -1062,7 +955,7 @@ func (r *Room) NotifySessionResumed(session *ClientSession) { } func (r *Room) NotifySessionChanged(session Session, flags SessionChangeFlag) { - if flags&SessionChangeFlags != 0 && session.ClientType() == api.HelloClientTypeVirtual { + if flags&SessionChangeFlags != 0 && session.ClientType() == HelloClientTypeVirtual { // Only notify if a virtual session has changed. if virtual, ok := session.(*VirtualSession); ok { r.publishSessionFlagsChanged(virtual) @@ -1071,8 +964,14 @@ func (r *Room) NotifySessionChanged(session Session, flags SessionChangeFlag) { if flags&SessionChangeInCall != 0 { joinLeave := 0 - if session, ok := session.(SessionWithInCall); ok { - if session.GetInCall()&FlagInCall != 0 { + if clientSession, ok := session.(*ClientSession); ok { + if clientSession.GetInCall()&FlagInCall != 0 { + joinLeave = 1 + } else { + joinLeave = 2 + } + } else if virtual, ok := session.(*VirtualSession); ok { + if virtual.GetInCall()&FlagInCall != 0 { joinLeave = 1 } else { joinLeave = 2 @@ -1080,13 +979,17 @@ func (r *Room) NotifySessionChanged(session Session, flags SessionChangeFlag) { } if joinLeave != 0 { - switch joinLeave { - case 1: - if r.addSessionToCall(session) { - r.logger.Printf("Session %s joined call %s", session.PublicId(), r.id) + if joinLeave == 1 { + r.mu.Lock() + if !r.inCallSessions[session] { + r.inCallSessions[session] = true + log.Printf("Session %s joined call %s", session.PublicId(), r.id) } - case 2: - r.removeSessionFromCall(session) + r.mu.Unlock() + } else if joinLeave == 2 { + r.mu.Lock() + delete(r.inCallSessions, session) + r.mu.Unlock() if clientSession, ok := session.(*ClientSession); ok { clientSession.LeaveCall() } @@ -1105,17 +1008,17 @@ func (r *Room) publishUsersChangedWithInternal() { } if err := r.publish(message); err != nil { - r.logger.Printf("Could not publish users changed message in room %s: %s", r.Id(), err) + log.Printf("Could not publish users changed message in room %s: %s", r.Id(), err) } } func (r *Room) publishSessionFlagsChanged(session *VirtualSession) { - message := &api.ServerMessage{ + message := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "participants", Type: "flags", - Flags: &api.RoomFlagsServerMessage{ + Flags: &RoomFlagsServerMessage{ RoomId: r.id, SessionId: session.PublicId(), Flags: session.Flags(), @@ -1123,7 +1026,7 @@ func (r *Room) publishSessionFlagsChanged(session *VirtualSession) { }, } if err := r.publish(message); err != nil { - r.logger.Printf("Could not publish flags changed message in room %s: %s", r.Id(), err) + log.Printf("Could not publish flags changed message in room %s: %s", r.Id(), err) } } @@ -1131,7 +1034,7 @@ func (r *Room) publishActiveSessions() (int, *sync.WaitGroup) { r.mu.RLock() defer r.mu.RUnlock() - entries := make(map[string][]talk.BackendPingEntry) + entries := make(map[string][]BackendPingEntry) urls := make(map[string]*url.URL) for _, session := range r.sessions { u := session.BackendUrl() @@ -1139,21 +1042,7 @@ func (r *Room) publishActiveSessions() (int, *sync.WaitGroup) { continue } - u += PathToOcsSignalingBackend - parsed, err := url.Parse(u) - if err != nil { - r.logger.Printf("Could not parse backend url %s: %s", u, err) - continue - } - - var changed bool - if parsed, changed = internal.CanonicalizeUrl(parsed); changed { - u = parsed.String() - } - - parsedBackendUrl := parsed - - var sid api.RoomSessionId + var sid string var uid string switch sess := session.(type) { case *ClientSession: @@ -1162,7 +1051,7 @@ func (r *Room) publishActiveSessions() (int, *sync.WaitGroup) { uid = sess.AuthUserId() case *VirtualSession: // Use our internal generated session id (will be added to Nextcloud). - sid = api.RoomSessionId(sess.PublicId()) + sid = sess.PublicId() uid = sess.UserId() default: continue @@ -1172,14 +1061,15 @@ func (r *Room) publishActiveSessions() (int, *sync.WaitGroup) { } e, found := entries[u] if !found { - if parsedBackendUrl == nil { + p := session.ParsedBackendUrl() + if p == nil { // Should not happen, invalid URLs should get rejected earlier. continue } - urls[u] = parsedBackendUrl + urls[u] = p } - entries[u] = append(e, talk.BackendPingEntry{ + entries[u] = append(e, BackendPingEntry{ SessionId: sid, UserId: uid, }) @@ -1189,101 +1079,106 @@ func (r *Room) publishActiveSessions() (int, *sync.WaitGroup) { return 0, &wg } var count int - ctx := log.NewLoggerContext(context.Background(), r.logger) for u, e := range entries { wg.Add(1) count += len(e) - go func(url *url.URL, entries []talk.BackendPingEntry) { + go func(url *url.URL, entries []BackendPingEntry) { defer wg.Done() - sendCtx, cancel := context.WithTimeout(ctx, r.hub.backendTimeout) + ctx, cancel := context.WithTimeout(context.Background(), r.hub.backendTimeout) defer cancel() - if err := r.hub.roomPing.SendPings(sendCtx, r.id, url, entries); err != nil { - r.logger.Printf("Error pinging room %s for active entries %+v: %s", r.id, entries, err) + if err := r.hub.roomPing.SendPings(ctx, r.id, url, entries); err != nil { + log.Printf("Error pinging room %s for active entries %+v: %s", r.id, entries, err) } }(urls[u], e) } return count, &wg } -func (r *Room) publishRoomMessage(message *talk.BackendRoomMessageRequest) { +func (r *Room) publishRoomMessage(message *BackendRoomMessageRequest) { if message == nil || len(message.Data) == 0 { return } - msg := &api.ServerMessage{ + msg := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "message", - Message: &api.RoomEventMessage{ + Message: &RoomEventMessage{ RoomId: r.id, Data: message.Data, }, }, } if err := r.publish(msg); err != nil { - r.logger.Printf("Could not publish room message in room %s: %s", r.Id(), err) + log.Printf("Could not publish room message in room %s: %s", r.Id(), err) } } -func (r *Room) publishSwitchTo(message *talk.BackendRoomSwitchToMessageRequest) { +func (r *Room) publishSwitchTo(message *BackendRoomSwitchToMessageRequest) { var wg sync.WaitGroup if len(message.SessionsList) > 0 { - msg := &api.ServerMessage{ + msg := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "switchto", - SwitchTo: &api.EventServerMessageSwitchTo{ + SwitchTo: &EventServerMessageSwitchTo{ RoomId: message.RoomId, }, }, } for _, sessionId := range message.SessionsList { - wg.Go(func() { - if err := r.events.PublishSessionMessage(sessionId, r.backend, &events.AsyncMessage{ + wg.Add(1) + go func(sessionId string) { + defer wg.Done() + + if err := r.events.PublishSessionMessage(sessionId, r.backend, &AsyncMessage{ Type: "message", Message: msg, }); err != nil { - r.logger.Printf("Error publishing switchto event to session %s: %s", sessionId, err) + log.Printf("Error publishing switchto event to session %s: %s", sessionId, err) } - }) + }(sessionId) } } if len(message.SessionsMap) > 0 { for sessionId, details := range message.SessionsMap { - wg.Go(func() { - msg := &api.ServerMessage{ + wg.Add(1) + go func(sessionId string, details json.RawMessage) { + defer wg.Done() + + msg := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "switchto", - SwitchTo: &api.EventServerMessageSwitchTo{ + SwitchTo: &EventServerMessageSwitchTo{ RoomId: message.RoomId, Details: details, }, }, } - if err := r.events.PublishSessionMessage(sessionId, r.backend, &events.AsyncMessage{ + if err := r.events.PublishSessionMessage(sessionId, r.backend, &AsyncMessage{ Type: "message", Message: msg, }); err != nil { - r.logger.Printf("Error publishing switchto event to session %s: %s", sessionId, err) + log.Printf("Error publishing switchto event to session %s: %s", sessionId, err) } - }) + }(sessionId, details) } } wg.Wait() } func (r *Room) notifyInternalRoomDeleted() { - msg := &api.ServerMessage{ + msg := &ServerMessage{ Type: "event", - Event: &api.EventServerMessage{ + Event: &EventServerMessage{ Target: "room", Type: "delete", }, @@ -1296,107 +1191,14 @@ func (r *Room) notifyInternalRoomDeleted() { } } -func (r *Room) SetTransientData(key string, value any) error { - if value == nil { - return r.RemoveTransientData(key) - } - - return r.events.PublishBackendRoomMessage(r.Id(), r.Backend(), &events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "transient", - Transient: &talk.BackendRoomTransientRequest{ - Action: talk.TransientActionSet, - Key: key, - Value: value, - }, - }, - }) -} - -func (r *Room) doSetTransientData(key string, value any) { +func (r *Room) SetTransientData(key string, value interface{}) { r.transientData.Set(key, value) } -func (r *Room) SetTransientDataTTL(key string, value any, ttl time.Duration) error { - if value == nil { - return r.RemoveTransientData(key) - } else if ttl == 0 { - return r.SetTransientData(key, value) - } - - return r.events.PublishBackendRoomMessage(r.Id(), r.Backend(), &events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "transient", - Transient: &talk.BackendRoomTransientRequest{ - Action: talk.TransientActionSet, - Key: key, - Value: value, - TTL: ttl, - }, - }, - }) -} - -func (r *Room) doSetTransientDataTTL(key string, value any, ttl time.Duration) { +func (r *Room) SetTransientDataTTL(key string, value interface{}, ttl time.Duration) { r.transientData.SetTTL(key, value, ttl) } -func (r *Room) RemoveTransientData(key string) error { - return r.events.PublishBackendRoomMessage(r.Id(), r.Backend(), &events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "transient", - Transient: &talk.BackendRoomTransientRequest{ - Action: talk.TransientActionDelete, - Key: key, - }, - }, - }) -} - -func (r *Room) doRemoveTransientData(key string) { +func (r *Room) RemoveTransientData(key string) { r.transientData.Remove(key) } - -func (r *Room) fetchInitialTransientData() { - if r.hub.rpcClients == nil { - return - } - - ctx := log.NewLoggerContext(context.Background(), r.logger) - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - - var wg sync.WaitGroup - var mu sync.Mutex - // +checklocks:mu - var initial api.TransientDataEntries - for _, client := range r.hub.rpcClients.GetClients() { - wg.Go(func() { - data, err := client.GetTransientData(ctx, r.Id(), r.Backend()) - if err != nil { - r.logger.Printf("Received error while getting transient data for %s@%s from %s: %s", r.Id(), r.Backend().Id(), client.Target(), err) - return - } else if len(data) == 0 { - return - } - - r.logger.Printf("Received initial transient data %+v from %s", data, client.Target()) - mu.Lock() - defer mu.Unlock() - if initial == nil { - initial = make(api.TransientDataEntries) - } - maps.Copy(initial, data) - }) - } - wg.Wait() - - mu.Lock() - defer mu.Unlock() - if len(initial) > 0 { - r.transientData.SetInitial(initial) - } -} diff --git a/server/room_ping.go b/room_ping.go similarity index 61% rename from server/room_ping.go rename to room_ping.go index c657364..5902068 100644 --- a/server/room_ping.go +++ b/room_ping.go @@ -19,36 +19,32 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" + "log" "net/url" - "slices" "sync" "time" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) type pingEntries struct { url *url.URL - entries map[string][]talk.BackendPingEntry + entries map[string][]BackendPingEntry } -func newPingEntries(url *url.URL, roomId string, entries []talk.BackendPingEntry) *pingEntries { +func newPingEntries(url *url.URL, roomId string, entries []BackendPingEntry) *pingEntries { return &pingEntries{ url: url, - entries: map[string][]talk.BackendPingEntry{ + entries: map[string][]BackendPingEntry{ roomId: entries, }, } } -func (e *pingEntries) Add(roomId string, entries []talk.BackendPingEntry) { +func (e *pingEntries) Add(roomId string, entries []BackendPingEntry) { if existing, found := e.entries[roomId]; found { e.entries[roomId] = append(existing, entries...) } else { @@ -68,19 +64,19 @@ func (e *pingEntries) RemoveRoom(roomId string) { // and sent out batched every "updateActiveSessionsInterval" seconds. type RoomPing struct { mu sync.Mutex - closer *internal.Closer + closer *Closer - hub *Hub - backend *talk.BackendClient + backend *BackendClient + capabilities *Capabilities - // +checklocks:mu entries map[string]*pingEntries } -func NewRoomPing(backend *talk.BackendClient) (*RoomPing, error) { +func NewRoomPing(backend *BackendClient, capabilities *Capabilities) (*RoomPing, error) { result := &RoomPing{ - closer: internal.NewCloser(), - backend: backend, + closer: NewCloser(), + backend: backend, + capabilities: capabilities, } return result, nil @@ -102,7 +98,7 @@ loop: case <-p.closer.C: break loop case <-ticker.C: - p.publishActiveSessions(context.Background()) + p.publishActiveSessions() } } } @@ -116,73 +112,80 @@ func (p *RoomPing) getAndClearEntries() map[string]*pingEntries { return entries } -func (p *RoomPing) publishEntries(ctx context.Context, entries *pingEntries, timeout time.Duration) { - ctx, cancel := context.WithTimeout(ctx, timeout) +func (p *RoomPing) publishEntries(entries *pingEntries, timeout time.Duration) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - limit, _, found := p.backend.GetIntegerConfig(ctx, entries.url, talk.ConfigGroupSignaling, talk.ConfigKeySessionPingLimit) + limit, _, found := p.capabilities.GetIntegerConfig(ctx, entries.url, ConfigGroupSignaling, ConfigKeySessionPingLimit) if !found || limit <= 0 { // Limit disabled while waiting for the next iteration, fallback to sending // one request per room. - logger := log.LoggerFromContext(ctx) for roomId, e := range entries.entries { - ctx2, cancel2 := context.WithTimeout(context.WithoutCancel(ctx), timeout) + ctx2, cancel2 := context.WithTimeout(context.Background(), timeout) defer cancel2() if err := p.sendPingsDirect(ctx2, roomId, entries.url, e); err != nil { - logger.Printf("Error pinging room %s for active entries %+v: %s", roomId, e, err) + log.Printf("Error pinging room %s for active entries %+v: %s", roomId, e, err) } } return } - var allEntries []talk.BackendPingEntry + var allEntries []BackendPingEntry for _, e := range entries.entries { allEntries = append(allEntries, e...) } - p.sendPingsCombined(ctx, entries.url, allEntries, limit, timeout) + p.sendPingsCombined(entries.url, allEntries, limit, timeout) } -func (p *RoomPing) publishActiveSessions(ctx context.Context) { +func (p *RoomPing) publishActiveSessions() { var timeout time.Duration - if p.hub != nil { - timeout = p.hub.backendTimeout + if p.backend.hub != nil { + timeout = p.backend.hub.backendTimeout } else { // Running from tests. timeout = time.Second * time.Duration(defaultBackendTimeoutSeconds) } entries := p.getAndClearEntries() var wg sync.WaitGroup + wg.Add(len(entries)) for _, e := range entries { - wg.Go(func() { - p.publishEntries(ctx, e, timeout) - }) + go func(e *pingEntries) { + defer wg.Done() + p.publishEntries(e, timeout) + }(e) } wg.Wait() } -func (p *RoomPing) sendPingsDirect(ctx context.Context, roomId string, url *url.URL, entries []talk.BackendPingEntry) error { - request := talk.NewBackendClientPingRequest(roomId, entries) - var response talk.BackendClientResponse +func (p *RoomPing) sendPingsDirect(ctx context.Context, roomId string, url *url.URL, entries []BackendPingEntry) error { + request := NewBackendClientPingRequest(roomId, entries) + var response BackendClientResponse return p.backend.PerformJSONRequest(ctx, url, request, &response) } -func (p *RoomPing) sendPingsCombined(ctx context.Context, url *url.URL, entries []talk.BackendPingEntry, limit int, timeout time.Duration) { - logger := log.LoggerFromContext(ctx) - for tosend := range slices.Chunk(entries, limit) { - subCtx, cancel := context.WithTimeout(ctx, timeout) +func (p *RoomPing) sendPingsCombined(url *url.URL, entries []BackendPingEntry, limit int, timeout time.Duration) { + total := len(entries) + for idx := 0; idx < total; idx += limit { + end := idx + limit + if end > total { + end = total + } + tosend := entries[idx:end] + + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - request := talk.NewBackendClientPingRequest("", tosend) - var response talk.BackendClientResponse - if err := p.backend.PerformJSONRequest(subCtx, url, request, &response); err != nil { - logger.Printf("Error sending combined ping session entries %+v to %s: %s", tosend, url, err) + request := NewBackendClientPingRequest("", tosend) + var response BackendClientResponse + if err := p.backend.PerformJSONRequest(ctx, url, request, &response); err != nil { + log.Printf("Error sending combined ping session entries %+v to %s: %s", tosend, url, err) } } } -func (p *RoomPing) SendPings(ctx context.Context, roomId string, url *url.URL, entries []talk.BackendPingEntry) error { - limit, _, found := p.backend.GetIntegerConfig(ctx, url, talk.ConfigGroupSignaling, talk.ConfigKeySessionPingLimit) +func (p *RoomPing) SendPings(ctx context.Context, roomId string, url *url.URL, entries []BackendPingEntry) error { + limit, _, found := p.capabilities.GetIntegerConfig(ctx, url, ConfigGroupSignaling, ConfigKeySessionPingLimit) if !found || limit <= 0 { // Old-style Nextcloud or session limit not configured. Perform one request // per room. Don't queue to avoid sending all ping requests to old-style diff --git a/server/room_ping_test.go b/room_ping_test.go similarity index 69% rename from server/room_ping_test.go rename to room_ping_test.go index 9572895..0bc775d 100644 --- a/server/room_ping_test.go +++ b/room_ping_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" @@ -30,13 +30,9 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) -func NewRoomPingForTest(ctx context.Context, t *testing.T) (*url.URL, *RoomPing) { +func NewRoomPingForTest(t *testing.T) (*url.URL, *RoomPing) { require := require.New(t) r := mux.NewRouter() registerBackendHandler(t, r) @@ -49,32 +45,30 @@ func NewRoomPingForTest(ctx context.Context, t *testing.T) (*url.URL, *RoomPing) config, err := getTestConfig(server) require.NoError(err) - backend, err := talk.NewBackendClient(ctx, config, 1, "0.0", nil) + backend, err := NewBackendClient(config, 1, "0.0", nil) require.NoError(err) - p, err := NewRoomPing(backend) + p, err := NewRoomPing(backend, backend.capabilities) require.NoError(err) - u, err := url.Parse(server.URL + "/" + PathToOcsSignalingBackend) + u, err := url.Parse(server.URL) require.NoError(err) return u, p } func TestSingleRoomPing(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) - u, ping := NewRoomPingForTest(ctx, t) + u, ping := NewRoomPingForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() room1 := &Room{ id: "sample-room-1", } - entries1 := []talk.BackendPingEntry{ + entries1 := []BackendPingEntry{ { UserId: "foo", SessionId: "123", @@ -89,7 +83,7 @@ func TestSingleRoomPing(t *testing.T) { room2 := &Room{ id: "sample-room-2", } - entries2 := []talk.BackendPingEntry{ + entries2 := []BackendPingEntry{ { UserId: "bar", SessionId: "456", @@ -101,24 +95,22 @@ func TestSingleRoomPing(t *testing.T) { } clearPingRequests(t) - ping.publishActiveSessions(ctx) + ping.publishActiveSessions() assert.Empty(getPingRequests(t)) } func TestMultiRoomPing(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) - u, ping := NewRoomPingForTest(ctx, t) + u, ping := NewRoomPingForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() room1 := &Room{ id: "sample-room-1", } - entries1 := []talk.BackendPingEntry{ + entries1 := []BackendPingEntry{ { UserId: "foo", SessionId: "123", @@ -130,7 +122,7 @@ func TestMultiRoomPing(t *testing.T) { room2 := &Room{ id: "sample-room-2", } - entries2 := []talk.BackendPingEntry{ + entries2 := []BackendPingEntry{ { UserId: "bar", SessionId: "456", @@ -139,26 +131,24 @@ func TestMultiRoomPing(t *testing.T) { assert.NoError(ping.SendPings(ctx, room2.Id(), u, entries2)) assert.Empty(getPingRequests(t)) - ping.publishActiveSessions(ctx) + ping.publishActiveSessions() if requests := getPingRequests(t); assert.Len(requests, 1) { assert.Len(requests[0].Ping.Entries, 2) } } func TestMultiRoomPing_Separate(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) - u, ping := NewRoomPingForTest(ctx, t) + u, ping := NewRoomPingForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() room1 := &Room{ id: "sample-room-1", } - entries1 := []talk.BackendPingEntry{ + entries1 := []BackendPingEntry{ { UserId: "foo", SessionId: "123", @@ -166,7 +156,7 @@ func TestMultiRoomPing_Separate(t *testing.T) { } assert.NoError(ping.SendPings(ctx, room1.Id(), u, entries1)) assert.Empty(getPingRequests(t)) - entries2 := []talk.BackendPingEntry{ + entries2 := []BackendPingEntry{ { UserId: "bar", SessionId: "456", @@ -175,26 +165,24 @@ func TestMultiRoomPing_Separate(t *testing.T) { assert.NoError(ping.SendPings(ctx, room1.Id(), u, entries2)) assert.Empty(getPingRequests(t)) - ping.publishActiveSessions(ctx) + ping.publishActiveSessions() if requests := getPingRequests(t); assert.Len(requests, 1) { assert.Len(requests[0].Ping.Entries, 2) } } func TestMultiRoomPing_DeleteRoom(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) + CatchLogForTest(t) assert := assert.New(t) - u, ping := NewRoomPingForTest(ctx, t) + u, ping := NewRoomPingForTest(t) - ctx, cancel := context.WithTimeout(ctx, testTimeout) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() room1 := &Room{ id: "sample-room-1", } - entries1 := []talk.BackendPingEntry{ + entries1 := []BackendPingEntry{ { UserId: "foo", SessionId: "123", @@ -206,7 +194,7 @@ func TestMultiRoomPing_DeleteRoom(t *testing.T) { room2 := &Room{ id: "sample-room-2", } - entries2 := []talk.BackendPingEntry{ + entries2 := []BackendPingEntry{ { UserId: "bar", SessionId: "456", @@ -217,7 +205,7 @@ func TestMultiRoomPing_DeleteRoom(t *testing.T) { ping.DeleteRoom(room2.Id()) - ping.publishActiveSessions(ctx) + ping.publishActiveSessions() if requests := getPingRequests(t); assert.Len(requests, 1) { assert.Len(requests[0].Ping.Entries, 1) } diff --git a/talk/backend_configuration_stats_prometheus.go b/room_stats_prometheus.go similarity index 69% rename from talk/backend_configuration_stats_prometheus.go rename to room_stats_prometheus.go index 9941db8..eec8a96 100644 --- a/talk/backend_configuration_stats_prometheus.go +++ b/room_stats_prometheus.go @@ -19,27 +19,25 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package talk +package signaling import ( "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" ) var ( - statsBackendsCurrent = prometheus.NewGauge(prometheus.GaugeOpts{ + statsRoomSessionsCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "signaling", - Subsystem: "backend", - Name: "current", - Help: "The current number of configured backends", - }) + Subsystem: "room", + Name: "sessions", + Help: "The current number of sessions in a room", + }, []string{"backend", "room", "clienttype"}) - backendConfigurationStats = []prometheus.Collector{ - statsBackendsCurrent, + roomStats = []prometheus.Collector{ + statsRoomSessionsCurrent, } ) -func RegisterBackendConfigurationStats() { - metrics.RegisterAll(backendConfigurationStats...) +func RegisterRoomStats() { + registerAll(roomStats...) } diff --git a/room_test.go b/room_test.go new file mode 100644 index 0000000..2d170ea --- /dev/null +++ b/room_test.go @@ -0,0 +1,483 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "testing" + "time" + + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRoom_InCall(t *testing.T) { + type Testcase struct { + Value interface{} + InCall bool + Valid bool + } + tests := []Testcase{ + {nil, false, false}, + {"a", false, false}, + {true, true, true}, + {false, false, true}, + {0, false, true}, + {FlagDisconnected, false, true}, + {1, true, true}, + {FlagInCall, true, true}, + {2, false, true}, + {FlagWithAudio, false, true}, + {3, true, true}, + {FlagInCall | FlagWithAudio, true, true}, + {4, false, true}, + {FlagWithVideo, false, true}, + {5, true, true}, + {FlagInCall | FlagWithVideo, true, true}, + {1.1, true, true}, + {json.Number("1"), true, true}, + {json.Number("1.1"), false, false}, + } + for _, test := range tests { + inCall, ok := IsInCall(test.Value) + if test.Valid { + assert.True(t, ok, "%+v should be valid", test.Value) + } else { + assert.False(t, ok, "%+v should not be valid", test.Value) + } + assert.EqualValues(t, test.InCall, inCall, "conversion failed for %+v", test.Value) + } +} + +func TestRoom_Update(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, router, server := CreateHubForTest(t) + + config, err := getTestConfig(server) + require.NoError(err) + b, err := NewBackendServer(config, hub, "no-version") + require.NoError(err) + require.NoError(b.Start(router)) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event. + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) + + // Simulate backend request from Nextcloud to update the room. + roomProperties := json.RawMessage("{\"foo\":\"bar\"}") + msg := &BackendServerRoomRequest{ + Type: "update", + Update: &BackendRoomUpdateRequest{ + UserIds: []string{ + testDefaultUserId, + }, + Properties: roomProperties, + }, + } + + data, err := json.Marshal(msg) + require.NoError(err) + res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data) + require.NoError(err) + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + assert.NoError(err) + assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) + + // The client receives a roomlist update and a changed room event. The + // ordering is not defined because messages are sent by asynchronous event + // handlers. + message1, err := client.RunUntilMessage(ctx) + assert.NoError(err) + message2, err := client.RunUntilMessage(ctx) + assert.NoError(err) + + if msg, err := checkMessageRoomlistUpdate(message1); err != nil { + assert.NoError(checkMessageRoomId(message1, roomId)) + if msg, err := checkMessageRoomlistUpdate(message2); assert.NoError(err) { + assert.Equal(roomId, msg.RoomId) + assert.Equal(string(roomProperties), string(msg.Properties)) + } + } else { + assert.Equal(roomId, msg.RoomId) + assert.Equal(string(roomProperties), string(msg.Properties)) + assert.NoError(checkMessageRoomId(message2, roomId)) + } + + // Allow up to 100 milliseconds for asynchronous event processing. + ctx2, cancel2 := context.WithTimeout(ctx, 100*time.Millisecond) + defer cancel2() + +loop: + for { + select { + case <-ctx2.Done(): + break loop + default: + // The internal room has been updated with the new properties. + if room := hub.getRoom(roomId); room == nil { + err = fmt.Errorf("Room %s not found in hub", roomId) + } else if len(room.Properties()) == 0 || !bytes.Equal(room.Properties(), roomProperties) { + err = fmt.Errorf("Expected room properties %s, got %+v", string(roomProperties), room.Properties()) + } else { + err = nil + } + } + if err == nil { + break + } + + time.Sleep(time.Millisecond) + } + + assert.NoError(err) +} + +func TestRoom_Delete(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, router, server := CreateHubForTest(t) + + config, err := getTestConfig(server) + require.NoError(err) + b, err := NewBackendServer(config, hub, "no-version") + require.NoError(err) + require.NoError(b.Start(router)) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event. + assert.NoError(client.RunUntilJoined(ctx, hello.Hello)) + + // Simulate backend request from Nextcloud to update the room. + msg := &BackendServerRoomRequest{ + Type: "delete", + Delete: &BackendRoomDeleteRequest{ + UserIds: []string{ + testDefaultUserId, + }, + }, + } + + data, err := json.Marshal(msg) + require.NoError(err) + res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data) + require.NoError(err) + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + assert.NoError(err) + assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) + + // The client is no longer invited to the room and leaves it. The ordering + // of messages is not defined as they get published through events and handled + // by asynchronous channels. + message1, err := client.RunUntilMessage(ctx) + assert.NoError(err) + + if err := checkMessageType(message1, "event"); err != nil { + // Ordering should be "leave room", "disinvited". + assert.NoError(checkMessageRoomId(message1, "")) + if message2, err := client.RunUntilMessage(ctx); assert.NoError(err) { + _, err := checkMessageRoomlistDisinvite(message2) + assert.NoError(err) + } + } else { + // Ordering should be "disinvited", "leave room". + _, err := checkMessageRoomlistDisinvite(message1) + assert.NoError(err) + message2, err := client.RunUntilMessage(ctx) + if err != nil { + // The connection should get closed after the "disinvited". + if websocket.IsUnexpectedCloseError(err, + websocket.CloseNormalClosure, + websocket.CloseGoingAway, + websocket.CloseNoStatusReceived) { + assert.NoError(err) + } + } else { + assert.NoError(checkMessageRoomId(message2, "")) + } + } + + // Allow up to 100 milliseconds for asynchronous event processing. + ctx2, cancel2 := context.WithTimeout(ctx, 100*time.Millisecond) + defer cancel2() + +loop: + for { + select { + case <-ctx2.Done(): + break loop + default: + // The internal room has been updated with the new properties. + hub.ru.Lock() + _, found := hub.rooms[roomId] + hub.ru.Unlock() + + if found { + err = fmt.Errorf("Room %s still found in hub", roomId) + } else { + err = nil + } + } + if err == nil { + break + } + + time.Sleep(time.Millisecond) + } + + assert.NoError(err) +} + +func TestRoom_RoomJoinFeatures(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, router, server := CreateHubForTest(t) + + config, err := getTestConfig(server) + require.NoError(err) + b, err := NewBackendServer(config, hub, "no-version") + require.NoError(err) + require.NoError(b.Start(router)) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + features := []string{"one", "two", "three"} + require.NoError(client.SendHelloClientWithFeatures(testDefaultUserId, features)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { + if assert.NoError(client.checkMessageJoinedSession(message, hello.Hello.SessionId, testDefaultUserId)) { + assert.Equal(roomId+"-"+hello.Hello.SessionId, message.Event.Join[0].RoomSessionId) + assert.Equal(features, message.Event.Join[0].Features) + } + } +} + +func TestRoom_RoomSessionData(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, router, server := CreateHubForTest(t) + + config, err := getTestConfig(server) + require.NoError(err) + b, err := NewBackendServer(config, hub, "no-version") + require.NoError(err) + require.NoError(b.Start(router)) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + + require.NoError(client.SendHello(authAnonymousUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello, err := client.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room-with-sessiondata" + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // We will receive a "joined" event with the userid from the room session data. + expected := "userid-from-sessiondata" + if message, err := client.RunUntilMessage(ctx); assert.NoError(err) { + if assert.NoError(client.checkMessageJoinedSession(message, hello.Hello.SessionId, expected)) { + assert.Equal(roomId+"-"+hello.Hello.SessionId, message.Event.Join[0].RoomSessionId) + } + } + + session := hub.GetSessionByPublicId(hello.Hello.SessionId) + require.NotNil(session, "Could not find session %s", hello.Hello.SessionId) + assert.Equal(expected, session.UserId()) + + room := hub.getRoom(roomId) + assert.NotNil(room, "Room not found") + + entries, wg := room.publishActiveSessions() + assert.Equal(1, entries) + wg.Wait() +} + +func TestRoom_InCallAll(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, router, server := CreateHubForTest(t) + + config, err := getTestConfig(server) + require.NoError(err) + b, err := NewBackendServer(config, hub, "no-version") + require.NoError(err) + require.NoError(b.Start(router)) + + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + + require.NoError(client1.SendHello(testDefaultUserId + "1")) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + assert.NoError(client1.RunUntilJoined(ctx, hello1.Hello)) + + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + assert.NoError(client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello)) + + assert.NoError(client1.RunUntilJoined(ctx, hello2.Hello)) + + // Simulate backend request from Nextcloud to update the "inCall" flag of all participants. + msg1 := &BackendServerRoomRequest{ + Type: "incall", + InCall: &BackendRoomInCallRequest{ + All: true, + InCall: json.RawMessage(strconv.FormatInt(FlagInCall, 10)), + }, + } + + data1, err := json.Marshal(msg1) + require.NoError(err) + res1, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data1) + require.NoError(err) + defer res1.Body.Close() + body1, err := io.ReadAll(res1.Body) + assert.NoError(err) + assert.Equal(http.StatusOK, res1.StatusCode, "Expected successful request, got %s", string(body1)) + + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(checkMessageInCallAll(msg, roomId, FlagInCall)) + } + + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(checkMessageInCallAll(msg, roomId, FlagInCall)) + } + + // Simulate backend request from Nextcloud to update the "inCall" flag of all participants. + msg2 := &BackendServerRoomRequest{ + Type: "incall", + InCall: &BackendRoomInCallRequest{ + All: true, + InCall: json.RawMessage(strconv.FormatInt(0, 10)), + }, + } + + data2, err := json.Marshal(msg2) + require.NoError(err) + res2, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data2) + require.NoError(err) + defer res2.Body.Close() + body2, err := io.ReadAll(res2.Body) + assert.NoError(err) + assert.Equal(http.StatusOK, res2.StatusCode, "Expected successful request, got %s", string(body2)) + + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(checkMessageInCallAll(msg, roomId, 0)) + } + + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(checkMessageInCallAll(msg, roomId, 0)) + } +} diff --git a/server/roomsessions.go b/roomsessions.go similarity index 69% rename from server/roomsessions.go rename to roomsessions.go index 9692655..b984463 100644 --- a/server/roomsessions.go +++ b/roomsessions.go @@ -19,23 +19,21 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" - "errors" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" + "fmt" ) var ( - ErrNoSuchRoomSession = errors.New("unknown room session id") + ErrNoSuchRoomSession = fmt.Errorf("unknown room session id") ) type RoomSessions interface { - SetRoomSession(session Session, roomSessionId api.RoomSessionId) error + SetRoomSession(session Session, roomSessionId string) error DeleteRoomSession(session Session) - GetSessionId(roomSessionId api.RoomSessionId) (api.PublicSessionId, error) - LookupSessionId(ctx context.Context, roomSessionId api.RoomSessionId, disconnectReason string) (api.PublicSessionId, error) + GetSessionId(roomSessionId string) (string, error) + LookupSessionId(ctx context.Context, roomSessionId string, disconnectReason string) (string, error) } diff --git a/server/roomsessions_builtin.go b/roomsessions_builtin.go similarity index 70% rename from server/roomsessions_builtin.go rename to roomsessions_builtin.go index fd1adcd..926fe9f 100644 --- a/server/roomsessions_builtin.go +++ b/roomsessions_builtin.go @@ -19,39 +19,34 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" "errors" + "log" "sync" "sync/atomic" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) type BuiltinRoomSessions struct { - mu sync.RWMutex - // +checklocks:mu - sessionIdToRoomSession map[api.PublicSessionId]api.RoomSessionId - // +checklocks:mu - roomSessionToSessionid map[api.RoomSessionId]api.PublicSessionId + sessionIdToRoomSession map[string]string + roomSessionToSessionid map[string]string + mu sync.RWMutex - clients *grpc.Clients + clients *GrpcClients } -func NewBuiltinRoomSessions(clients *grpc.Clients) (RoomSessions, error) { +func NewBuiltinRoomSessions(clients *GrpcClients) (RoomSessions, error) { return &BuiltinRoomSessions{ - sessionIdToRoomSession: make(map[api.PublicSessionId]api.RoomSessionId), - roomSessionToSessionid: make(map[api.RoomSessionId]api.PublicSessionId), + sessionIdToRoomSession: make(map[string]string), + roomSessionToSessionid: make(map[string]string), clients: clients, }, nil } -func (r *BuiltinRoomSessions) SetRoomSession(session Session, roomSessionId api.RoomSessionId) error { +func (r *BuiltinRoomSessions) SetRoomSession(session Session, roomSessionId string) error { if roomSessionId == "" { r.DeleteRoomSession(session) return nil @@ -89,7 +84,7 @@ func (r *BuiltinRoomSessions) DeleteRoomSession(session Session) { } } -func (r *BuiltinRoomSessions) GetSessionId(roomSessionId api.RoomSessionId) (api.PublicSessionId, error) { +func (r *BuiltinRoomSessions) GetSessionId(roomSessionId string) (string, error) { r.mu.RLock() defer r.mu.RUnlock() sid, found := r.roomSessionToSessionid[roomSessionId] @@ -100,7 +95,7 @@ func (r *BuiltinRoomSessions) GetSessionId(roomSessionId api.RoomSessionId) (api return sid, nil } -func (r *BuiltinRoomSessions) LookupSessionId(ctx context.Context, roomSessionId api.RoomSessionId, disconnectReason string) (api.PublicSessionId, error) { +func (r *BuiltinRoomSessions) LookupSessionId(ctx context.Context, roomSessionId string, disconnectReason string) (string, error) { sid, err := r.GetSessionId(roomSessionId) if err == nil { return sid, nil @@ -120,23 +115,25 @@ func (r *BuiltinRoomSessions) LookupSessionId(ctx context.Context, roomSessionId var wg sync.WaitGroup var result atomic.Value - logger := log.LoggerFromContext(ctx) for _, client := range clients { - wg.Go(func() { + wg.Add(1) + go func(client *GrpcClient) { + defer wg.Done() + sid, err := client.LookupSessionId(lookupctx, roomSessionId, disconnectReason) if errors.Is(err, context.Canceled) { return } else if err != nil { - logger.Printf("Received error while checking for room session id %s on %s: %s", roomSessionId, client.Target(), err) + log.Printf("Received error while checking for room session id %s on %s: %s", roomSessionId, client.Target(), err) return } else if sid == "" { - logger.Printf("Received empty session id for room session id %s from %s", roomSessionId, client.Target()) + log.Printf("Received empty session id for room session id %s from %s", roomSessionId, client.Target()) return } cancel() // Cancel pending RPC calls. result.Store(sid) - }) + }(client) } wg.Wait() @@ -145,5 +142,5 @@ func (r *BuiltinRoomSessions) LookupSessionId(ctx context.Context, roomSessionId return "", ErrNoSuchRoomSession } - return value.(api.PublicSessionId), nil + return value.(string), nil } diff --git a/server/roomsessions_builtin_test.go b/roomsessions_builtin_test.go similarity index 97% rename from server/roomsessions_builtin_test.go rename to roomsessions_builtin_test.go index 1d84bf6..c69e346 100644 --- a/server/roomsessions_builtin_test.go +++ b/roomsessions_builtin_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "testing" @@ -28,7 +28,6 @@ import ( ) func TestBuiltinRoomSessions(t *testing.T) { - t.Parallel() sessions, err := NewBuiltinRoomSessions(nil) require.NoError(t, err) diff --git a/server/roomsessions_test.go b/roomsessions_test.go similarity index 72% rename from server/roomsessions_test.go rename to roomsessions_test.go index d8ee8b0..6501819 100644 --- a/server/roomsessions_test.go +++ b/roomsessions_test.go @@ -19,44 +19,39 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" "encoding/json" "net/url" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/session" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) type DummySession struct { - publicId api.PublicSessionId + publicId string } func (s *DummySession) Context() context.Context { return context.Background() } -func (s *DummySession) PrivateId() api.PrivateSessionId { +func (s *DummySession) PrivateId() string { return "" } -func (s *DummySession) PublicId() api.PublicSessionId { +func (s *DummySession) PublicId() string { return s.publicId } -func (s *DummySession) ClientType() api.ClientType { +func (s *DummySession) ClientType() string { return "" } -func (s *DummySession) Data() *session.SessionIdData { +func (s *DummySession) Data() *SessionIdData { return nil } @@ -68,11 +63,11 @@ func (s *DummySession) UserData() json.RawMessage { return nil } -func (s *DummySession) ParsedUserData() (api.StringMap, error) { +func (s *DummySession) ParsedUserData() (map[string]interface{}, error) { return nil, nil } -func (s *DummySession) Backend() *talk.Backend { +func (s *DummySession) Backend() *Backend { return nil } @@ -84,17 +79,13 @@ func (s *DummySession) ParsedBackendUrl() *url.URL { return nil } -func (s *DummySession) SetRoom(room *Room, joinTime time.Time) { +func (s *DummySession) SetRoom(room *Room) { } func (s *DummySession) GetRoom() *Room { return nil } -func (s *DummySession) IsInRoom(id string) bool { - return false -} - func (s *DummySession) LeaveRoom(notify bool) *Room { return nil } @@ -102,19 +93,19 @@ func (s *DummySession) LeaveRoom(notify bool) *Room { func (s *DummySession) Close() { } -func (s *DummySession) HasPermission(permission api.Permission) bool { +func (s *DummySession) HasPermission(permission Permission) bool { return false } -func (s *DummySession) SendError(e *api.Error) bool { +func (s *DummySession) SendError(e *Error) bool { return false } -func (s *DummySession) SendMessage(message *api.ServerMessage) bool { +func (s *DummySession) SendMessage(message *ServerMessage) bool { return false } -func checkSession(t *testing.T, sessions RoomSessions, sessionId api.PublicSessionId, roomSessionId api.RoomSessionId) Session { +func checkSession(t *testing.T, sessions RoomSessions, sessionId string, roomSessionId string) Session { session := &DummySession{ publicId: sessionId, } @@ -128,7 +119,7 @@ func checkSession(t *testing.T, sessions RoomSessions, sessionId api.PublicSessi func testRoomSessions(t *testing.T, sessions RoomSessions) { assert := assert.New(t) if sid, err := sessions.GetSessionId("unknown"); err == nil { - assert.Fail("Expected error about invalid room session", "got session id %s", sid) + assert.Fail("Expected error about invalid room session, got session id %s", sid) } else { assert.ErrorIs(err, ErrNoSuchRoomSession) } @@ -142,7 +133,7 @@ func testRoomSessions(t *testing.T, sessions RoomSessions) { sessions.DeleteRoomSession(s1) if sid, err := sessions.GetSessionId("room1"); err == nil { - assert.Fail("Expected error about invalid room session", "got session id %s", sid) + assert.Fail("Expected error about invalid room session, got session id %s", sid) } else { assert.ErrorIs(err, ErrNoSuchRoomSession) } @@ -159,7 +150,7 @@ func testRoomSessions(t *testing.T, sessions RoomSessions) { assert.NoError(sessions.SetRoomSession(s2, "room-session2")) if sid, err := sessions.GetSessionId("room-session"); err == nil { - assert.Fail("Expected error about invalid room session", "got session id %s", sid) + assert.Fail("Expected error about invalid room session, got session id %s", sid) } else { assert.ErrorIs(err, ErrNoSuchRoomSession) } diff --git a/scripts/get-version.sh b/scripts/get-version.sh index 28074ab..e1fc562 100755 --- a/scripts/get-version.sh +++ b/scripts/get-version.sh @@ -26,4 +26,4 @@ if [ -z "$VERSION" ]; then VERSION=unknown fi -echo "$VERSION" +echo $VERSION diff --git a/scripts/get_continent_map.py b/scripts/get_continent_map.py index d86cf3d..61ef9c4 100755 --- a/scripts/get_continent_map.py +++ b/scripts/get_continent_map.py @@ -87,13 +87,13 @@ def generate_map(filename): continents.setdefault(country, []).append(continent) out = StringIO() - out.write('package geoip\n') + out.write('package signaling\n') out.write('\n') out.write('// This file has been automatically generated, do not modify.\n') out.write('// Source: %s\n' % (URL)) out.write('\n') out.write('var (\n') - out.write('\tContinentMap = map[Country][]Continent{\n') + out.write('\tContinentMap = map[string][]string{\n') for country, continents in sorted(continents.items()): value = [] for continent in continents: diff --git a/scripts/prepare-changelog.py b/scripts/prepare-changelog.py deleted file mode 100755 index 7fa3011..0000000 --- a/scripts/prepare-changelog.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/python3 - -import git -import os.path -import re - -ROOT = os.path.join(os.path.dirname(__file__), '..') - -FIND_PR = re.compile(r'Merge pull request #(\d+) from').findall - -ENTRY = """- %(title)s - [#%(pr)s](https://github.com/strukturag/nextcloud-spreed-signaling/pull/%(pr)s)""" - -SIMPLE_ENTRY = """- %(title)s""" - -def main(): - repo = git.Repo(ROOT) - latest = repo.tags[-1] - print('Generating changelog since %s (commit %s)' % (latest, latest.commit)) - - entries = [] - dependencies = [] - ignore = set() - for commit in repo.iter_commits('%s..HEAD' % (latest.commit, )): - if len(commit.parents) > 1: - # Merge commit. - ignore.add(commit.parents[-1]) - entries.append(commit) - else: - try: - ignore.remove(commit) - except KeyError: - # Direct commit. - entries.append(commit) - else: - # Commit is part of a merge. - ignore.add(commit.parents[0]) - - # Sort commits from old to new. - for commit in reversed(entries): - lines = [x.strip() for x in commit.message.strip().split('\n')] - title = None - for line in lines: - if not line: - title = '' - elif title == '': - title = line - break - - if not title and len(lines) == 1: - title = lines[0] - assert line, (commit.message, ) - pr = FIND_PR(commit.summary) - assert len(pr) <= 1, (commit.summary, ) - if len(pr) == 1: - entry = ENTRY % { - 'title': title, - 'pr': pr[0], - } - elif len(pr) == 0: - entry = SIMPLE_ENTRY % { - 'title': title, - } - - if title.startswith('Bump '): - dependencies.append(entry) - else: - print(entry) - - # Dependencies should be in a separate section at the end. - if dependencies: - print() - print('### Dependencies') - print('\n'.join(dependencies)) - -if __name__ == '__main__': - main() diff --git a/security/certificate_reloader_test.go b/security/certificate_reloader_test.go deleted file mode 100644 index b74d780..0000000 --- a/security/certificate_reloader_test.go +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package security - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "path" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" -) - -type withReloadCounter interface { - GetReloadCounter() uint64 -} - -func waitForReload(ctx context.Context, t *testing.T, r withReloadCounter, expected uint64) bool { - t.Helper() - - for r.GetReloadCounter() < expected { - if !assert.NoError(t, ctx.Err()) { - return false - } - - time.Sleep(time.Millisecond) - } - return true -} - -func TestCertificateReloader(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - key, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - - org1 := "Testing certificate" - cert1 := internal.GenerateSelfSignedCertificateForTesting(t, org1, key) - - tmpdir := t.TempDir() - certFile := path.Join(tmpdir, "cert.pem") - privkeyFile := path.Join(tmpdir, "privkey.pem") - pubkeyFile := path.Join(tmpdir, "pubkey.pem") - - require.NoError(internal.WritePrivateKey(key, privkeyFile)) - require.NoError(internal.WritePublicKey(&key.PublicKey, pubkeyFile)) - require.NoError(internal.WriteCertificate(cert1, certFile)) - - logger := logtest.NewLoggerForTest(t) - reloader, err := NewCertificateReloader(logger, certFile, privkeyFile) - require.NoError(err) - - defer reloader.Close() - - if cert, err := reloader.GetCertificate(nil); assert.NoError(err) { - assert.True(cert1.Equal(cert.Leaf)) - assert.True(key.Equal(cert.PrivateKey)) - } - if cert, err := reloader.GetClientCertificate(nil); assert.NoError(err) { - assert.True(cert1.Equal(cert.Leaf)) - assert.True(key.Equal(cert.PrivateKey)) - } - - ctx, cancel := context.WithTimeout(t.Context(), time.Second) - defer cancel() - - org2 := "Updated certificate" - cert2 := internal.GenerateSelfSignedCertificateForTesting(t, org2, key) - internal.ReplaceCertificate(t, certFile, cert2) - - waitForReload(ctx, t, reloader, 1) - - if cert, err := reloader.GetCertificate(nil); assert.NoError(err) { - assert.True(cert2.Equal(cert.Leaf)) - assert.True(key.Equal(cert.PrivateKey)) - } - if cert, err := reloader.GetClientCertificate(nil); assert.NoError(err) { - assert.True(cert2.Equal(cert.Leaf)) - assert.True(key.Equal(cert.PrivateKey)) - } -} - -func TestCertPoolReloader(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - - key, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - - org1 := "Testing certificate" - cert1 := internal.GenerateSelfSignedCertificateForTesting(t, org1, key) - - tmpdir := t.TempDir() - certFile := path.Join(tmpdir, "cert.pem") - privkeyFile := path.Join(tmpdir, "privkey.pem") - pubkeyFile := path.Join(tmpdir, "pubkey.pem") - - require.NoError(internal.WritePrivateKey(key, privkeyFile)) - require.NoError(internal.WritePublicKey(&key.PublicKey, pubkeyFile)) - require.NoError(internal.WriteCertificate(cert1, certFile)) - - logger := logtest.NewLoggerForTest(t) - reloader, err := NewCertPoolReloader(logger, certFile) - require.NoError(err) - - defer reloader.Close() - - pool1 := reloader.GetCertPool() - assert.NotNil(pool1) - - ctx, cancel := context.WithTimeout(t.Context(), time.Second) - defer cancel() - - org2 := "Updated certificate" - cert2 := internal.GenerateSelfSignedCertificateForTesting(t, org2, key) - internal.ReplaceCertificate(t, certFile, cert2) - - waitForReload(ctx, t, reloader, 1) - - pool2 := reloader.GetCertPool() - assert.NotNil(pool2) - - assert.False(pool1.Equal(pool2)) -} diff --git a/server.conf.in b/server.conf.in index 2e0a7cf..85630d5 100644 --- a/server.conf.in +++ b/server.conf.in @@ -25,9 +25,7 @@ certificate = /etc/nginx/ssl/server.crt key = /etc/nginx/ssl/server.key [app] -# Set to "true" to install pprof debug handlers. Access will only be possible -# from IPs allowed through the "allowed_ips" option below. -# +# Set to "true" to install pprof debug handlers. # See "https://golang.org/pkg/net/http/pprof/" for further information. debug = false @@ -86,8 +84,7 @@ internalsecret = the-shared-secret-for-internal-clients # For backend type "etcd": # Key prefix of backend entries. All keys below will be watched and assumed to # contain a JSON document with the following entries: -# - "urls": List of urls of the Nextcloud instance. -# - "url": Url of the Nextcloud instance (deprecated). +# - "url": Url of the Nextcloud instance. # - "secret": Shared secret for requests from and to the backend servers. # # Additional optional entries: @@ -96,8 +93,8 @@ internalsecret = the-shared-secret-for-internal-clients # - "sessionlimit": Number of sessions that are allowed to connect. # # Example: -# "/signaling/backend/one" -> {"urls": ["https://nextcloud.domain1.invalid"], ...} -# "/signaling/backend/two" -> {"urls": ["https://domain2.invalid/nextcloud"], ...} +# "/signaling/backend/one" -> {"url": "https://nextcloud.domain1.invalid", ...} +# "/signaling/backend/two" -> {"url": "https://domain2.invalid/nextcloud", ...} #backendprefix = /signaling/backend # Allow any hostname as backend endpoint. This is extremely insecure and should @@ -125,8 +122,8 @@ connectionsperhost = 8 # Backend configurations as defined in the "[backend]" section above. The # section names must match the ids used in "backends" above. #[backend-id] -# Comma-separated list of urls of the Nextcloud instance -#urls = https://cloud.domain.invalid +# URL of the Nextcloud instance +#url = https://cloud.domain.invalid # Shared secret for requests from and to the backend servers. Leave empty to use # the common shared secret from above. @@ -146,8 +143,8 @@ connectionsperhost = 8 #maxscreenbitrate = 2097152 #[another-backend] -# Comma-separated list of urls of the Nextcloud instance -#urls = https://cloud.otherdomain.invalid +# URL of the Nextcloud instance +#url = https://cloud.otherdomain.invalid # Shared secret for requests from and to the backend servers. Leave empty to use # the common shared secret from above. @@ -182,13 +179,6 @@ connectionsperhost = 8 # proxy server that is used. #maxscreenbitrate = 2097152 -# List of IP addresses / subnets that are allowed to be used by clients in -# candidates. The allowed list has preference over the blocked list below. -#allowedcandidates = 10.0.0.0/8 - -# List of IP addresses / subnets to filter from candidates received by clients. -#blockedcandidates = 1.2.3.0/24 - # For type "proxy": timeout in seconds for requests to the proxy server. #proxytimeout = 2 @@ -272,9 +262,8 @@ connectionsperhost = 8 #SA = NA [stats] -# Comma-separated list of IP addresses that are allowed to access the debug, -# stats and metrics endpoints. -# Leave empty (or commented) to only allow access from localhost. +# Comma-separated list of IP addresses that are allowed to access the stats +# endpoint. Leave empty (or commented) to only allow access from "127.0.0.1". #allowed_ips = [etcd] diff --git a/server/clientsession.go b/server/clientsession.go deleted file mode 100644 index 2ad59c1..0000000 --- a/server/clientsession.go +++ /dev/null @@ -1,1691 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "maps" - "net/url" - "slices" - "sync" - "sync/atomic" - "time" - - "github.com/pion/sdp/v3" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - "github.com/strukturag/nextcloud-spreed-signaling/v2/session" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -var ( - // Warn if a session has 32 or more pending messages. - warnPendingMessagesCount = 32 - - // The "/api/v1/signaling/" URL will be changed to use "v3" as the "signaling-v3" - // feature is returned by the capabilities endpoint. - PathToOcsSignalingBackend = "ocs/v2.php/apps/spreed/api/v1/signaling/backend" -) - -// ResponseHandlerFunc will return "true" has been fully processed. -type ResponseHandlerFunc func(message *api.ClientMessage) bool - -type ClientSession struct { - logger log.Logger - hub *Hub - events events.AsyncEvents - privateId api.PrivateSessionId - publicId api.PublicSessionId - data *session.SessionIdData - ctx context.Context - closeFunc context.CancelFunc - - clientType api.ClientType - features []string - userId string - userData json.RawMessage - - parseUserData func() (api.StringMap, error) - - inCall internal.Flags - // +checklocks:mu - supportsPermissions bool - // +checklocks:mu - permissions map[api.Permission]bool - - backend *talk.Backend - backendUrl string - parsedBackendUrl *url.URL - - mu sync.Mutex - asyncCh events.AsyncChannel - - // +checklocks:mu - client ClientWithSession - room atomic.Pointer[Room] - roomJoinTime atomic.Int64 - federation atomic.Pointer[FederationClient] - - roomSessionIdLock sync.RWMutex - // +checklocks:roomSessionIdLock - roomSessionId api.RoomSessionId - - publishersCond sync.Cond - - // +checklocks:mu - publishers map[sfu.StreamType]sfu.Publisher - // +checklocks:mu - subscribers map[sfu.StreamId]sfu.Subscriber - - // +checklocks:mu - pendingClientMessages []*api.ServerMessage - // +checklocks:mu - hasPendingChat bool - // +checklocks:mu - hasPendingParticipantsUpdate bool - - // +checklocks:mu - virtualSessions map[*VirtualSession]bool - - filterDuplicateLock sync.Mutex - // +checklocks:filterDuplicateLock - seenJoinedEvents map[api.PublicSessionId]bool - // +checklocks:filterDuplicateLock - seenFlags map[api.PublicSessionId]uint32 - - responseHandlersLock sync.Mutex - // +checklocks:responseHandlersLock - responseHandlers map[string]ResponseHandlerFunc -} - -func NewClientSession(hub *Hub, privateId api.PrivateSessionId, publicId api.PublicSessionId, data *session.SessionIdData, backend *talk.Backend, hello *api.HelloClientMessage, auth *talk.BackendClientAuthResponse) (*ClientSession, error) { - ctx := log.NewLoggerContext(context.Background(), hub.logger) - ctx, closeFunc := context.WithCancel(ctx) - s := &ClientSession{ - logger: hub.logger, - hub: hub, - events: hub.events, - privateId: privateId, - publicId: publicId, - data: data, - ctx: ctx, - closeFunc: closeFunc, - - clientType: hello.Auth.Type, - features: hello.Features, - userId: auth.UserId, - userData: auth.User, - parseUserData: parseUserData(auth.User), - - backend: backend, - asyncCh: make(events.AsyncChannel, events.DefaultAsyncChannelSize), - } - s.publishersCond.L = &s.mu - if s.clientType == api.HelloClientTypeInternal { - s.backendUrl = hello.Auth.InternalParams.Backend - s.parsedBackendUrl = hello.Auth.InternalParams.ParsedBackend - if !s.HasFeature(api.ClientFeatureInternalInCall) { - s.SetInCall(FlagInCall | FlagWithAudio) - } - } else { - s.backendUrl = hello.Auth.Url - s.parsedBackendUrl = hello.Auth.ParsedUrl - } - - if err := s.SubscribeEvents(); err != nil { - return nil, err - } - go s.run() - return s, nil -} - -func (s *ClientSession) Context() context.Context { - return s.ctx -} - -func (s *ClientSession) PrivateId() api.PrivateSessionId { - return s.privateId -} - -func (s *ClientSession) PublicId() api.PublicSessionId { - return s.publicId -} - -func (s *ClientSession) RoomSessionId() api.RoomSessionId { - s.roomSessionIdLock.RLock() - defer s.roomSessionIdLock.RUnlock() - return s.roomSessionId -} - -func (s *ClientSession) Data() *session.SessionIdData { - return s.data -} - -func (s *ClientSession) ClientType() api.ClientType { - return s.clientType -} - -// GetInCall is only used for internal clients. -func (s *ClientSession) GetInCall() int { - return int(s.inCall.Get()) -} - -func (s *ClientSession) SetInCall(inCall int) bool { - if inCall < 0 { - inCall = 0 - } - - return s.inCall.Set(uint32(inCall)) -} - -func (s *ClientSession) GetFeatures() []string { - return s.features -} - -func (s *ClientSession) HasFeature(feature string) bool { - return slices.Contains(s.features, feature) -} - -// HasPermission checks if the session has the passed permissions. -func (s *ClientSession) HasPermission(permission api.Permission) bool { - s.mu.Lock() - defer s.mu.Unlock() - - return s.hasPermissionLocked(permission) -} - -func (s *ClientSession) GetPermissions() []api.Permission { - s.mu.Lock() - defer s.mu.Unlock() - - result := make([]api.Permission, len(s.permissions)) - for p, ok := range s.permissions { - if ok { - result = append(result, p) - } - } - return result -} - -// HasAnyPermission checks if the session has one of the passed permissions. -func (s *ClientSession) HasAnyPermission(permission ...api.Permission) bool { - if len(permission) == 0 { - return false - } - - s.mu.Lock() - defer s.mu.Unlock() - - return s.hasAnyPermissionLocked(permission...) -} - -// +checklocks:s.mu -func (s *ClientSession) hasAnyPermissionLocked(permission ...api.Permission) bool { - if len(permission) == 0 { - return false - } - - return slices.ContainsFunc(permission, s.hasPermissionLocked) -} - -// +checklocks:s.mu -func (s *ClientSession) hasPermissionLocked(permission api.Permission) bool { - if !s.supportsPermissions { - // Old-style session that doesn't receive permissions from Nextcloud. - if result, found := api.DefaultPermissionOverrides[permission]; found { - return result - } - return true - } - - if val, found := s.permissions[permission]; found { - return val - } - return false -} - -func (s *ClientSession) SetPermissions(permissions []api.Permission) { - var p map[api.Permission]bool - for _, permission := range permissions { - if p == nil { - p = make(map[api.Permission]bool) - } - p[permission] = true - } - - s.mu.Lock() - defer s.mu.Unlock() - if s.supportsPermissions && maps.Equal(s.permissions, p) { - return - } - - s.permissions = p - s.supportsPermissions = true - s.logger.Printf("Permissions of session %s changed: %s", s.PublicId(), permissions) -} - -func (s *ClientSession) Backend() *talk.Backend { - return s.backend -} - -func (s *ClientSession) BackendUrl() string { - return s.backendUrl -} - -func (s *ClientSession) ParsedBackendUrl() *url.URL { - return s.parsedBackendUrl -} - -func (s *ClientSession) ParsedBackendOcsUrl() *url.URL { - return s.parsedBackendUrl.JoinPath(PathToOcsSignalingBackend) -} - -func (s *ClientSession) AuthUserId() string { - return s.userId -} - -func (s *ClientSession) UserId() string { - userId := s.userId - if userId == "" { - if room := s.GetRoom(); room != nil { - if data := room.GetRoomSessionData(s); data != nil { - userId = data.UserId - } - } - } - return userId -} - -func (s *ClientSession) UserData() json.RawMessage { - return s.userData -} - -func (s *ClientSession) ParsedUserData() (api.StringMap, error) { - return s.parseUserData() -} - -func (s *ClientSession) SetRoom(room *Room, joinTime time.Time) { - s.room.Store(room) - s.onRoomSet(room != nil, joinTime) -} - -func (s *ClientSession) onRoomSet(hasRoom bool, joinTime time.Time) { - if hasRoom { - s.roomJoinTime.Store(joinTime.UnixNano()) - } else { - s.roomJoinTime.Store(0) - } - - s.filterDuplicateLock.Lock() - defer s.filterDuplicateLock.Unlock() - s.seenJoinedEvents = nil - s.seenFlags = nil -} - -func (s *ClientSession) IsInRoom(id string) bool { - room := s.GetRoom() - return room != nil && room.Id() == id -} - -func (s *ClientSession) GetFederationClient() *FederationClient { - return s.federation.Load() -} - -func (s *ClientSession) SetFederationClient(federation *FederationClient) { - s.mu.Lock() - defer s.mu.Unlock() - - s.doLeaveRoom(true) - s.onRoomSet(federation != nil, time.Now()) - - if prev := s.federation.Swap(federation); prev != nil && prev != federation { - prev.Close() - } -} - -func (s *ClientSession) GetRoom() *Room { - return s.room.Load() -} - -func (s *ClientSession) getRoomJoinTime() time.Time { - t := s.roomJoinTime.Load() - if t == 0 { - return time.Time{} - } - - return time.Unix(0, t) -} - -// +checklocks:s.mu -func (s *ClientSession) releaseMcuObjects() { - if len(s.publishers) > 0 { - go func(publishers map[sfu.StreamType]sfu.Publisher) { - ctx := context.Background() - for _, publisher := range publishers { - publisher.Close(ctx) - } - }(s.publishers) - s.publishers = nil - } - if len(s.subscribers) > 0 { - go func(subscribers map[sfu.StreamId]sfu.Subscriber) { - ctx := context.Background() - for _, subscriber := range subscribers { - subscriber.Close(ctx) - } - }(s.subscribers) - s.subscribers = nil - } -} - -func (s *ClientSession) AsyncChannel() events.AsyncChannel { - return s.asyncCh -} - -func (s *ClientSession) run() { - for { - select { - case <-s.ctx.Done(): - return - case msg := <-s.asyncCh: - s.processAsyncNatsMessage(msg) - for count := len(s.asyncCh); count > 0; count-- { - s.processAsyncNatsMessage(<-s.asyncCh) - } - } - } -} - -func (s *ClientSession) Close() { - s.closeAndWait(true) -} - -func (s *ClientSession) closeAndWait(wait bool) { - s.closeFunc() - s.hub.removeSession(s) - - if prev := s.federation.Swap(nil); prev != nil { - prev.Close() - } - - s.mu.Lock() - defer s.mu.Unlock() - if s.userId != "" { - if err := s.events.UnregisterUserListener(s.userId, s.backend, s); err != nil && !errors.Is(err, nats.ErrConnectionClosed) { - s.logger.Printf("Error unsubscribing user listener for %s in session %s: %s", s.userId, s.publicId, err) - } - } - if err := s.events.UnregisterSessionListener(s.publicId, s.backend, s); err != nil && !errors.Is(err, nats.ErrConnectionClosed) { - s.logger.Printf("Error unsubscribing listener in session %s: %s", s.publicId, err) - } - go func(virtualSessions map[*VirtualSession]bool) { - for session := range virtualSessions { - session.Close() - } - }(s.virtualSessions) - s.virtualSessions = nil - s.releaseMcuObjects() - s.clearClientLocked(nil) - s.backend.RemoveSession(s) -} - -func (s *ClientSession) SubscribeEvents() error { - s.mu.Lock() - defer s.mu.Unlock() - - if s.userId != "" { - if err := s.events.RegisterUserListener(s.userId, s.backend, s); err != nil { - return err - } - } - - return s.events.RegisterSessionListener(s.publicId, s.backend, s) -} - -func (s *ClientSession) UpdateRoomSessionId(roomSessionId api.RoomSessionId) error { - s.roomSessionIdLock.Lock() - defer s.roomSessionIdLock.Unlock() - - if s.roomSessionId == roomSessionId { - return nil - } - - if err := s.hub.roomSessions.SetRoomSession(s, roomSessionId); err != nil { - return err - } - - if roomSessionId != "" { - if room := s.GetRoom(); room != nil { - s.logger.Printf("Session %s updated room session id to %s in room %s", s.PublicId(), roomSessionId, room.Id()) - } else if client := s.GetFederationClient(); client != nil { - s.logger.Printf("Session %s updated room session id to %s in federated room %s", s.PublicId(), roomSessionId, client.RemoteRoomId()) - } else { - s.logger.Printf("Session %s updated room session id to %s in unknown room", s.PublicId(), roomSessionId) - } - } else { - if room := s.GetRoom(); room != nil { - s.logger.Printf("Session %s cleared room session id in room %s", s.PublicId(), room.Id()) - } else if client := s.GetFederationClient(); client != nil { - s.logger.Printf("Session %s cleared room session id in federated room %s", s.PublicId(), client.RemoteRoomId()) - } else { - s.logger.Printf("Session %s cleared room session id in unknown room", s.PublicId()) - } - } - - s.roomSessionId = roomSessionId - return nil -} - -func (s *ClientSession) SubscribeRoomEvents(roomid string, roomSessionId api.RoomSessionId) error { - s.roomSessionIdLock.Lock() - defer s.roomSessionIdLock.Unlock() - - if err := s.events.RegisterRoomListener(roomid, s.backend, s); err != nil { - return err - } - - if roomSessionId != "" { - if err := s.hub.roomSessions.SetRoomSession(s, roomSessionId); err != nil { - s.doUnsubscribeRoomEvents(true) - return err - } - } - s.logger.Printf("Session %s joined room %s with room session id %s", s.PublicId(), roomid, roomSessionId) - s.roomSessionId = roomSessionId - return nil -} - -func (s *ClientSession) LeaveCall() { - s.mu.Lock() - defer s.mu.Unlock() - - room := s.GetRoom() - if room == nil { - return - } - - s.logger.Printf("Session %s left call %s", s.PublicId(), room.Id()) - s.releaseMcuObjects() -} - -func (s *ClientSession) LeaveRoom(notify bool) *Room { - return s.LeaveRoomWithMessage(notify, nil) -} - -func (s *ClientSession) LeaveRoomWithMessage(notify bool, message *api.ClientMessage) *Room { - if prev := s.federation.Swap(nil); prev != nil { - // Session was connected to a federation room. - if err := prev.Leave(message); err != nil { - s.logger.Printf("Error leaving room for session %s on federation client %s: %s", s.PublicId(), prev.URL(), err) - prev.Close() - } - return nil - } - - s.mu.Lock() - defer s.mu.Unlock() - - return s.doLeaveRoom(notify) -} - -// +checklocks:s.mu -func (s *ClientSession) doLeaveRoom(notify bool) *Room { - room := s.GetRoom() - if room == nil { - return nil - } - - s.doUnsubscribeRoomEvents(notify) - s.SetRoom(nil, time.Time{}) - s.releaseMcuObjects() - room.RemoveSession(s) - return room -} - -func (s *ClientSession) UnsubscribeRoomEvents() { - s.mu.Lock() - defer s.mu.Unlock() - - s.doUnsubscribeRoomEvents(true) -} - -func (s *ClientSession) doUnsubscribeRoomEvents(notify bool) { - room := s.GetRoom() - if room != nil { - if err := s.events.UnregisterRoomListener(room.Id(), s.Backend(), s); err != nil && !errors.Is(err, nats.ErrConnectionClosed) { - s.logger.Printf("Error unsubscribing room listener for %s in session %s: %s", room.Id(), s.publicId, err) - } - } - s.hub.roomSessions.DeleteRoomSession(s) - - s.roomSessionIdLock.Lock() - defer s.roomSessionIdLock.Unlock() - if notify && room != nil && s.roomSessionId != "" && !s.roomSessionId.IsFederated() { - // Notify - go func(sid api.RoomSessionId) { - ctx := log.NewLoggerContext(context.Background(), s.logger) - request := talk.NewBackendClientRoomRequest(room.Id(), s.userId, sid) - request.Room.UpdateFromSession(s) - request.Room.Action = "leave" - var response api.StringMap - if err := s.hub.backend.PerformJSONRequest(ctx, s.ParsedBackendOcsUrl(), request, &response); err != nil { - s.logger.Printf("Could not notify about room session %s left room %s: %s", sid, room.Id(), err) - } else { - s.logger.Printf("Removed room session %s: %+v", sid, response) - } - }(s.roomSessionId) - } - s.roomSessionId = "" -} - -func (s *ClientSession) ClearClient(client ClientWithSession) { - s.mu.Lock() - defer s.mu.Unlock() - - s.clearClientLocked(client) -} - -// +checklocks:s.mu -func (s *ClientSession) clearClientLocked(client ClientWithSession) { - if s.client == nil { - return - } else if client != nil && s.client != client { - s.logger.Printf("Trying to clear other client in session %s", s.PublicId()) - return - } - - prevClient := s.client - s.client = nil - prevClient.SetSession(nil) -} - -func (s *ClientSession) GetClient() ClientWithSession { - s.mu.Lock() - defer s.mu.Unlock() - - return s.getClientUnlocked() -} - -// +checklocks:s.mu -func (s *ClientSession) getClientUnlocked() ClientWithSession { - return s.client -} - -func (s *ClientSession) SetClient(client ClientWithSession) ClientWithSession { - if client == nil { - panic("Use ClearClient to set the client to nil") - } - - s.mu.Lock() - defer s.mu.Unlock() - - if client == s.client { - // No change - return nil - } - - client.SetSession(s) - prev := s.client - if prev != nil { - s.clearClientLocked(prev) - } - s.client = client - return prev -} - -// +checklocks:s.mu -func (s *ClientSession) sendOffer(client sfu.Client, sender api.PublicSessionId, streamType sfu.StreamType, offer api.StringMap) { - offer_message := &api.AnswerOfferMessage{ - To: s.PublicId(), - From: sender, - Type: "offer", - RoomType: string(streamType), - Payload: offer, - Sid: client.Sid(), - } - offer_data, err := json.Marshal(offer_message) - if err != nil { - s.logger.Println("Could not serialize offer", offer_message, err) - return - } - response_message := &api.ServerMessage{ - Type: "message", - Message: &api.MessageServerMessage{ - Sender: &api.MessageServerMessageSender{ - Type: "session", - SessionId: sender, - }, - Data: offer_data, - }, - } - - s.sendMessageUnlocked(response_message) -} - -// +checklocks:s.mu -func (s *ClientSession) sendCandidate(client sfu.Client, sender api.PublicSessionId, streamType sfu.StreamType, candidate any) { - candidate_message := &api.AnswerOfferMessage{ - To: s.PublicId(), - From: sender, - Type: "candidate", - RoomType: string(streamType), - Payload: api.StringMap{ - "candidate": candidate, - }, - Sid: client.Sid(), - } - candidate_data, err := json.Marshal(candidate_message) - if err != nil { - s.logger.Println("Could not serialize candidate", candidate_message, err) - return - } - response_message := &api.ServerMessage{ - Type: "message", - Message: &api.MessageServerMessage{ - Sender: &api.MessageServerMessageSender{ - Type: "session", - SessionId: sender, - }, - Data: candidate_data, - }, - } - - s.sendMessageUnlocked(response_message) -} - -// +checklocks:s.mu -func (s *ClientSession) sendMessageUnlocked(message *api.ServerMessage) { - if c := s.getClientUnlocked(); c != nil { - if c.SendMessage(message) { - return - } - } - - s.storePendingMessage(message) -} - -func (s *ClientSession) SendError(e *api.Error) bool { - message := &api.ServerMessage{ - Type: "error", - Error: e, - } - return s.SendMessage(message) -} - -func (s *ClientSession) SendMessage(message *api.ServerMessage) bool { - message, messages := s.filterMessage(message) - if message == nil && len(messages) == 0 { - return true - } - - s.mu.Lock() - defer s.mu.Unlock() - - if message != nil { - s.sendMessageUnlocked(message) - } - for _, msg := range messages { - s.sendMessageUnlocked(msg) - } - return true -} - -func (s *ClientSession) SendMessages(messages []*api.ServerMessage) bool { - s.mu.Lock() - defer s.mu.Unlock() - - for _, message := range messages { - s.sendMessageUnlocked(message) - } - return true -} - -func (s *ClientSession) OnUpdateOffer(client sfu.Client, offer api.StringMap) { - s.mu.Lock() - defer s.mu.Unlock() - - for _, sub := range s.subscribers { - if sub.Id() == client.Id() { - s.sendOffer(client, sub.Publisher(), client.StreamType(), offer) - return - } - } -} - -func (s *ClientSession) OnIceCandidate(client sfu.Client, candidate any) { - s.mu.Lock() - defer s.mu.Unlock() - - for _, sub := range s.subscribers { - if sub.Id() == client.Id() { - s.sendCandidate(client, sub.Publisher(), client.StreamType(), candidate) - return - } - } - - for _, pub := range s.publishers { - if pub.Id() == client.Id() { - s.sendCandidate(client, s.PublicId(), client.StreamType(), candidate) - return - } - } - - s.logger.Printf("Session %s received candidate %+v for unknown client %s", s.PublicId(), candidate, client.Id()) -} - -func (s *ClientSession) OnIceCompleted(client sfu.Client) { - // TODO(jojo): This causes a JavaScript error when creating a candidate from "null". - // Figure out a better way to signal this. - - // An empty candidate signals the end of candidates. - // s.OnIceCandidate(client, nil) -} - -func (s *ClientSession) SubscriberSidUpdated(subscriber sfu.Subscriber) { -} - -func (s *ClientSession) PublisherClosed(publisher sfu.Publisher) { - s.mu.Lock() - defer s.mu.Unlock() - - for id, p := range s.publishers { - if p == publisher { - delete(s.publishers, id) - break - } - } -} - -func (s *ClientSession) SubscriberClosed(subscriber sfu.Subscriber) { - s.mu.Lock() - defer s.mu.Unlock() - - for id, sub := range s.subscribers { - if sub == subscriber { - delete(s.subscribers, id) - break - } - } -} - -type PermissionError struct { - permission api.Permission -} - -func (e *PermissionError) Permission() api.Permission { - return e.permission -} - -func (e *PermissionError) Error() string { - return fmt.Sprintf("permission \"%s\" not found", e.permission) -} - -// +checklocks:s.mu -func (s *ClientSession) isSdpAllowedToSendLocked(sdp *sdp.SessionDescription) (sfu.MediaType, error) { - if sdp == nil { - // Should have already been checked when data was validated. - return 0, api.ErrNoSdp - } - - var mediaTypes sfu.MediaType - mayPublishMedia := s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_MEDIA) - for _, md := range sdp.MediaDescriptions { - switch md.MediaName.Media { - case "audio": - if !mayPublishMedia && !s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_AUDIO) { - return 0, &PermissionError{api.PERMISSION_MAY_PUBLISH_AUDIO} - } - - mediaTypes |= sfu.MediaTypeAudio - case "video": - if !mayPublishMedia && !s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_VIDEO) { - return 0, &PermissionError{api.PERMISSION_MAY_PUBLISH_VIDEO} - } - - mediaTypes |= sfu.MediaTypeVideo - } - } - - return mediaTypes, nil -} - -func (s *ClientSession) IsAllowedToSend(data *api.MessageClientMessageData) error { - s.mu.Lock() - defer s.mu.Unlock() - - switch { - case data != nil && data.RoomType == "screen": - if s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_SCREEN) { - return nil - } - return &PermissionError{api.PERMISSION_MAY_PUBLISH_SCREEN} - case s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_MEDIA): - // Client is allowed to publish any media (audio / video). - return nil - case data != nil && data.Type == "offer": - // Check what user is trying to publish and check permissions accordingly. - if _, err := s.isSdpAllowedToSendLocked(data.OfferSdp); err != nil { - return err - } - - return nil - default: - // Candidate or unknown event, check if client is allowed to publish any media. - if s.hasAnyPermissionLocked(api.PERMISSION_MAY_PUBLISH_AUDIO, api.PERMISSION_MAY_PUBLISH_VIDEO) { - return nil - } - - return errors.New("permission check failed") - } -} - -func (s *ClientSession) CheckOfferType(streamType sfu.StreamType, data *api.MessageClientMessageData) (sfu.MediaType, error) { - s.mu.Lock() - defer s.mu.Unlock() - - return s.checkOfferTypeLocked(streamType, data) -} - -// +checklocks:s.mu -func (s *ClientSession) checkOfferTypeLocked(streamType sfu.StreamType, data *api.MessageClientMessageData) (sfu.MediaType, error) { - if streamType == sfu.StreamTypeScreen { - if !s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_SCREEN) { - return 0, &PermissionError{api.PERMISSION_MAY_PUBLISH_SCREEN} - } - - return sfu.MediaTypeScreen, nil - } else if data != nil && data.Type == "offer" { - mediaTypes, err := s.isSdpAllowedToSendLocked(data.OfferSdp) - if err != nil { - return 0, err - } - - return mediaTypes, nil - } - - return 0, nil -} - -func (s *ClientSession) GetOrCreatePublisher(ctx context.Context, mcu sfu.SFU, streamType sfu.StreamType, data *api.MessageClientMessageData) (sfu.Publisher, error) { - s.mu.Lock() - defer s.mu.Unlock() - - mediaTypes, err := s.checkOfferTypeLocked(streamType, data) - if err != nil { - return nil, err - } - - publisher, found := s.publishers[streamType] - if !found { - client := s.getClientUnlocked() - s.mu.Unlock() - defer s.mu.Lock() - - settings := sfu.NewPublisherSettings{ - Bitrate: data.Bitrate, - MediaTypes: mediaTypes, - - AudioCodec: data.AudioCodec, - VideoCodec: data.VideoCodec, - VP9Profile: data.VP9Profile, - H264Profile: data.H264Profile, - } - if backend := s.Backend(); backend != nil { - var maxBitrate api.Bandwidth - if streamType == sfu.StreamTypeScreen { - maxBitrate = backend.MaxScreenBitrate() - } else { - maxBitrate = backend.MaxStreamBitrate() - } - if settings.Bitrate <= 0 { - settings.Bitrate = maxBitrate - } else if maxBitrate > 0 && settings.Bitrate > maxBitrate { - settings.Bitrate = maxBitrate - } - } - var err error - publisher, err = mcu.NewPublisher(ctx, s, s.PublicId(), data.Sid, streamType, settings, client) - if err != nil { - return nil, err - } - s.mu.Lock() - defer s.mu.Unlock() - if s.publishers == nil { - s.publishers = make(map[sfu.StreamType]sfu.Publisher) - } - if prev, found := s.publishers[streamType]; found { - // Another thread created the publisher while we were waiting. - go func(pub sfu.Publisher) { - closeCtx := context.Background() - pub.Close(closeCtx) - }(publisher) - publisher = prev - } else { - s.publishers[streamType] = publisher - } - s.logger.Printf("Publishing %s as %s for session %s", streamType, publisher.Id(), s.PublicId()) - s.publishersCond.Broadcast() - } else { - publisher.SetMedia(mediaTypes) - } - - return publisher, nil -} - -// +checklocks:s.mu -func (s *ClientSession) getPublisherLocked(streamType sfu.StreamType) sfu.Publisher { - return s.publishers[streamType] -} - -func (s *ClientSession) GetPublisher(streamType sfu.StreamType) sfu.Publisher { - s.mu.Lock() - defer s.mu.Unlock() - - return s.getPublisherLocked(streamType) -} - -func (s *ClientSession) GetOrWaitForPublisher(ctx context.Context, streamType sfu.StreamType) sfu.Publisher { - s.mu.Lock() - defer s.mu.Unlock() - - publisher := s.getPublisherLocked(streamType) - if publisher != nil { - return publisher - } - - stop := context.AfterFunc(ctx, func() { - s.publishersCond.Broadcast() - }) - defer stop() - - for publisher == nil { - if err := ctx.Err(); err != nil { - return nil - } - - s.publishersCond.Wait() - publisher = s.getPublisherLocked(streamType) - } - return publisher -} - -func (s *ClientSession) GetOrCreateSubscriber(ctx context.Context, mcu sfu.SFU, id api.PublicSessionId, streamType sfu.StreamType) (sfu.Subscriber, error) { - s.mu.Lock() - defer s.mu.Unlock() - - // TODO(jojo): Add method to remove subscribers. - - subscriber, found := s.subscribers[sfu.GetStreamId(id, streamType)] - if !found { - client := s.getClientUnlocked() - s.mu.Unlock() - var err error - subscriber, err = mcu.NewSubscriber(ctx, s, id, streamType, client) - s.mu.Lock() - if err != nil { - return nil, err - } - if s.subscribers == nil { - s.subscribers = make(map[sfu.StreamId]sfu.Subscriber) - } - if prev, found := s.subscribers[sfu.GetStreamId(id, streamType)]; found { - // Another thread created the subscriber while we were waiting. - go func(sub sfu.Subscriber) { - closeCtx := context.Background() - sub.Close(closeCtx) - }(subscriber) - subscriber = prev - } else { - s.subscribers[sfu.GetStreamId(id, streamType)] = subscriber - } - s.logger.Printf("Subscribing %s from %s as %s in session %s", streamType, id, subscriber.Id(), s.PublicId()) - } - - return subscriber, nil -} - -func (s *ClientSession) GetSubscriber(id api.PublicSessionId, streamType sfu.StreamType) sfu.Subscriber { - s.mu.Lock() - defer s.mu.Unlock() - - return s.subscribers[sfu.GetStreamId(id, streamType)] -} - -func (s *ClientSession) processAsyncNatsMessage(msg *nats.Msg) { - var message events.AsyncMessage - if err := nats.Decode(msg, &message); err != nil { - s.logger.Printf("Could not decode NATS message %+v: %s", msg, err) - return - } - - s.processAsyncMessage(&message) -} - -func (s *ClientSession) processAsyncMessage(message *events.AsyncMessage) { - switch message.Type { - case "permissions": - s.SetPermissions(message.Permissions) - go func() { - s.mu.Lock() - defer s.mu.Unlock() - - if !s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_MEDIA) { - if publisher, found := s.publishers[sfu.StreamTypeVideo]; found { - if (publisher.HasMedia(sfu.MediaTypeAudio) && !s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_AUDIO)) || - (publisher.HasMedia(sfu.MediaTypeVideo) && !s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_VIDEO)) { - delete(s.publishers, sfu.StreamTypeVideo) - s.logger.Printf("Session %s is no longer allowed to publish media, closing publisher %s", s.PublicId(), publisher.Id()) - go func() { - publisher.Close(context.Background()) - }() - return - } - } - } - if !s.hasPermissionLocked(api.PERMISSION_MAY_PUBLISH_SCREEN) { - if publisher, found := s.publishers[sfu.StreamTypeScreen]; found { - delete(s.publishers, sfu.StreamTypeScreen) - s.logger.Printf("Session %s is no longer allowed to publish screen, closing publisher %s", s.PublicId(), publisher.Id()) - go func() { - publisher.Close(context.Background()) - }() - return - } - } - }() - return - case "message": - if message.Message.Type == "bye" && message.Message.Bye.Reason == "room_session_reconnected" { - s.logger.Printf("Closing session %s because same room session %s connected", s.PublicId(), s.RoomSessionId()) - s.LeaveRoom(false) - defer s.closeAndWait(false) - } - case "sendoffer": - // Process asynchronously to not block other messages received. - go func() { - ctx, cancel := context.WithTimeout(s.Context(), s.hub.mcuTimeout) - defer cancel() - - mc, err := s.GetOrCreateSubscriber(ctx, s.hub.mcu, message.SendOffer.SessionId, sfu.StreamType(message.SendOffer.Data.RoomType)) - if err != nil { - s.logger.Printf("Could not create MCU subscriber for session %s to process sendoffer in %s: %s", message.SendOffer.SessionId, s.PublicId(), err) - if err := s.events.PublishSessionMessage(message.SendOffer.SessionId, s.backend, &events.AsyncMessage{ - Type: "message", - Message: &api.ServerMessage{ - Id: message.SendOffer.MessageId, - Type: "error", - Error: api.NewError("client_not_found", "No MCU client found to send message to."), - }, - }); err != nil { - s.logger.Printf("Error sending sendoffer error response to %s: %s", message.SendOffer.SessionId, err) - } - return - } else if mc == nil { - s.logger.Printf("No MCU subscriber found for session %s to process sendoffer in %s", message.SendOffer.SessionId, s.PublicId()) - if err := s.events.PublishSessionMessage(message.SendOffer.SessionId, s.backend, &events.AsyncMessage{ - Type: "message", - Message: &api.ServerMessage{ - Id: message.SendOffer.MessageId, - Type: "error", - Error: api.NewError("client_not_found", "No MCU client found to send message to."), - }, - }); err != nil { - s.logger.Printf("Error sending sendoffer error response to %s: %s", message.SendOffer.SessionId, err) - } - return - } - - mc.SendMessage(s.Context(), nil, message.SendOffer.Data, func(err error, response api.StringMap) { - if err != nil { - s.logger.Printf("Could not send MCU message %+v for session %s to %s: %s", message.SendOffer.Data, message.SendOffer.SessionId, s.PublicId(), err) - if err := s.events.PublishSessionMessage(message.SendOffer.SessionId, s.backend, &events.AsyncMessage{ - Type: "message", - Message: &api.ServerMessage{ - Id: message.SendOffer.MessageId, - Type: "error", - Error: api.NewError("processing_failed", "Processing of the message failed, please check server logs."), - }, - }); err != nil { - s.logger.Printf("Error sending sendoffer error response to %s: %s", message.SendOffer.SessionId, err) - } - return - } else if response == nil { - // No response received - return - } - - s.hub.sendMcuMessageResponse(s, mc, &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - SessionId: message.SendOffer.SessionId, - }, - }, message.SendOffer.Data, response) - }) - }() - return - } - - serverMessage := s.filterAsyncMessage(message) - if serverMessage == nil { - return - } - - s.SendMessage(serverMessage) -} - -// +checklocks:s.mu -func (s *ClientSession) storePendingMessage(message *api.ServerMessage) { - if message.IsChatRefresh() { - if s.hasPendingChat { - // Only send a single "chat-refresh" message on resume. - return - } - - s.hasPendingChat = true - } - if !s.hasPendingParticipantsUpdate && message.IsParticipantsUpdate() { - s.hasPendingParticipantsUpdate = true - } - s.pendingClientMessages = append(s.pendingClientMessages, message) - if len(s.pendingClientMessages) >= warnPendingMessagesCount { - s.logger.Printf("Session %s has %d pending messages", s.PublicId(), len(s.pendingClientMessages)) - } -} - -func filterDisplayNames(events []api.EventServerMessageSessionEntry) []api.EventServerMessageSessionEntry { - result := make([]api.EventServerMessageSessionEntry, 0, len(events)) - for _, event := range events { - if len(event.User) == 0 { - result = append(result, event) - continue - } - - var userdata api.StringMap - if err := json.Unmarshal(event.User, &userdata); err != nil { - result = append(result, event) - continue - } - - if _, found := userdata["displayname"]; !found { - result = append(result, event) - continue - } - - delete(userdata, "displayname") - if len(userdata) == 0 { - // No more userdata, no need to serialize empty map. - e := event.Clone() - e.User = nil - result = append(result, e) - continue - } - - data, err := json.Marshal(userdata) - if err != nil { - result = append(result, event) - continue - } - - e := event.Clone() - e.User = data - result = append(result, e) - } - return result -} - -// +checklocks:s.filterDuplicateLock -func (s *ClientSession) filterUnknownLeave(entries []api.PublicSessionId) []api.PublicSessionId { - idx := slices.IndexFunc(entries, func(e api.PublicSessionId) bool { - return !s.seenJoinedEvents[e] // +checklocksignore - }) - if idx == -1 { - return entries - } else if idx+1 == len(entries) { - // Simple case: all entries filtered. - s.logger.Printf("Session %s got unknown leave events for %+v", s.publicId, entries) - return nil - } - - // Filter remaining entries. - filtered := []api.PublicSessionId{ - entries[idx], - } - result := append([]api.PublicSessionId{}, entries[:idx]...) - for _, e := range entries[idx+1:] { - if s.seenJoinedEvents[e] { - result = append(result, e) - } else { - filtered = append(filtered, e) - } - } - s.logger.Printf("Session %s got unknown leave events for %+v", s.publicId, filtered) - return result -} - -func (s *ClientSession) filterDuplicateJoin(entries []api.EventServerMessageSessionEntry) []api.EventServerMessageSessionEntry { - s.filterDuplicateLock.Lock() - defer s.filterDuplicateLock.Unlock() - - // Due to the asynchronous events, a session might received a "Joined" event - // for the same (other) session twice, so filter these out on a per-session - // level. - result := make([]api.EventServerMessageSessionEntry, 0, len(entries)) - for _, e := range entries { - if s.seenJoinedEvents[e.SessionId] { - s.logger.Printf("Session %s got duplicate joined event for %s, ignoring", s.publicId, e.SessionId) - continue - } - - if s.seenJoinedEvents == nil { - s.seenJoinedEvents = make(map[api.PublicSessionId]bool) - } - s.seenJoinedEvents[e.SessionId] = true - result = append(result, e) - } - return result -} - -func (s *ClientSession) filterDuplicateFlags(message *api.RoomFlagsServerMessage) bool { - if message == nil { - return true - } - - s.filterDuplicateLock.Lock() - defer s.filterDuplicateLock.Unlock() - - // Due to the asynchronous events, a session might received a "flags" event - // for the same (other) session twice, so filter these out on a per-session - // level. - if prev, found := s.seenFlags[message.SessionId]; found && prev == message.Flags { - s.logger.Printf("Session %s got duplicate flags event for %s, ignoring", s.publicId, message.SessionId) - return true - } - - if s.seenFlags == nil { - s.seenFlags = make(map[api.PublicSessionId]uint32) - } - s.seenFlags[message.SessionId] = message.Flags - return false -} - -func (s *ClientSession) filterMessage(message *api.ServerMessage) (*api.ServerMessage, []*api.ServerMessage) { - switch message.Type { - case "event": - switch message.Event.Target { - case "participants": - switch message.Event.Type { - case "update": - m := message.Event.Update - users := make(map[any]bool) - for _, entry := range m.Users { - users[entry["sessionId"]] = true - } - for _, entry := range m.Changed { - if users[entry["sessionId"]] { - continue - } - m.Users = append(m.Users, entry) - } - // TODO(jojo): Only send all users if current session id has - // changed its "inCall" flag to true. - m.Changed = nil - case "flags": - if s.filterDuplicateFlags(message.Event.Flags) { - return nil, nil - } - } - case "room": - switch message.Event.Type { - case "join": - join := s.filterDuplicateJoin(message.Event.Join) - if len(join) == 0 { - return nil, nil - } - copied := false - if len(join) != len(message.Event.Join) { - // Create unique copy of message for only this client. - copied = true - message = &api.ServerMessage{ - Id: message.Id, - Type: message.Type, - Event: &api.EventServerMessage{ - Type: message.Event.Type, - Target: message.Event.Target, - Join: join, - }, - } - } - - if s.HasPermission(api.PERMISSION_HIDE_DISPLAYNAMES) { - if copied { - message.Event.Join = filterDisplayNames(message.Event.Join) - } else { - message = &api.ServerMessage{ - Id: message.Id, - Type: message.Type, - Event: &api.EventServerMessage{ - Type: message.Event.Type, - Target: message.Event.Target, - Join: filterDisplayNames(message.Event.Join), - }, - } - } - } - case "leave": - s.filterDuplicateLock.Lock() - defer s.filterDuplicateLock.Unlock() - - leave := s.filterUnknownLeave(message.Event.Leave) - if len(leave) == 0 { - return nil, nil - } - - for _, e := range message.Event.Leave { - delete(s.seenJoinedEvents, e) - delete(s.seenFlags, e) - } - - if len(leave) != len(message.Event.Leave) { - message = &api.ServerMessage{ - Id: message.Id, - Type: message.Type, - Event: &api.EventServerMessage{ - Type: message.Event.Type, - Target: message.Event.Target, - Leave: leave, - }, - } - } - case "message": - if message.Event.Message == nil || len(message.Event.Message.Data) == 0 { - return message, nil - } - - data, err := message.Event.Message.GetData() - if data == nil || err != nil { - return message, nil - } - - if data.Type == "chat" && data.Chat != nil { - update := false - if data.Chat.Refresh && data.Chat.HasComment() { - // New-style chat event, check what the client supports. - if s.HasFeature(api.ClientFeatureChatRelay) { - data.Chat.Refresh = false - } else { - data.Chat.Comment = nil - data.Chat.Comments = nil - } - update = true - } - - if data.Chat.HasComment() { - data.Chat.Comments = slices.DeleteFunc(data.Chat.Comments, func(comment json.RawMessage) bool { - return len(comment) == 0 - }) - if len(data.Chat.Comment) > 0 { - if len(data.Chat.Comments) == 0 { - data.Chat.Comments = []json.RawMessage{data.Chat.Comment} - } else { - data.Chat.Comments = append([]json.RawMessage{data.Chat.Comment}, data.Chat.Comments...) - } - data.Chat.Comment = nil - } - if len(data.Chat.Comments) > 0 && s.HasPermission(api.PERMISSION_HIDE_DISPLAYNAMES) { - for i, commentData := range data.Chat.Comments { - var comment api.ChatComment - if err := json.Unmarshal(commentData, &comment); err != nil { - continue - } - - if displayName, found := comment["actorDisplayName"]; found && displayName != "" { - comment["actorDisplayName"] = "" - var err error - if commentData, err = json.Marshal(comment); err != nil { - continue - } - data.Chat.Comments[i] = commentData - update = true - } - } - } - } - - if update || len(data.Chat.Comments) > 0 { - if len(data.Chat.Comment) == 0 && len(data.Chat.Comments) == 0 { - if encoded, err := json.Marshal(data); err == nil { - // Create unique copy of message for only this client. - message = &api.ServerMessage{ - Id: message.Id, - Type: message.Type, - Event: &api.EventServerMessage{ - Type: message.Event.Type, - Target: message.Event.Target, - Message: &api.RoomEventMessage{ - RoomId: message.Event.Message.RoomId, - Data: encoded, - }, - }, - } - } - } else { - // Forward different chat comments individually. - var result []*api.ServerMessage - for _, comment := range data.Chat.Comments { - commentData := api.RoomEventMessageData{ - Type: data.Type, - Chat: &api.RoomEventMessageDataChat{ - Refresh: data.Chat.Refresh, - Comment: comment, - }, - } - if encoded, err := json.Marshal(commentData); err == nil { - // Create unique copy of message for only this client. - result = append(result, &api.ServerMessage{ - Id: message.Id, - Type: message.Type, - Event: &api.EventServerMessage{ - Type: message.Event.Type, - Target: message.Event.Target, - Message: &api.RoomEventMessage{ - RoomId: message.Event.Message.RoomId, - Data: encoded, - }, - }, - }) - } - } - return nil, result - } - } - } - } - } - case "message": - if message.Message != nil && len(message.Message.Data) > 0 && s.HasPermission(api.PERMISSION_HIDE_DISPLAYNAMES) { - var data api.MessageServerMessageData - if err := json.Unmarshal(message.Message.Data, &data); err != nil { - return message, nil - } - - if data.Type == "nickChanged" { - return nil, nil - } - } - } - - return message, nil -} - -func (s *ClientSession) filterAsyncMessage(msg *events.AsyncMessage) *api.ServerMessage { - switch msg.Type { - case "message": - if msg.Message == nil { - s.logger.Printf("Received asynchronous message without payload: %+v", msg) - return nil - } - - switch msg.Message.Type { - case "message": - if msg.Message.Message != nil { - if sender := msg.Message.Message.Sender; sender != nil { - if sender.SessionId == s.PublicId() { - // Don't send message back to sender (can happen if sent to user or room) - return nil - } - if sender.Type == api.RecipientTypeCall { - if room := s.GetRoom(); room == nil || !room.IsSessionInCall(s) { - // Session is not in call, so discard. - return nil - } - } - } - } - case "control": - if msg.Message.Control != nil { - if sender := msg.Message.Control.Sender; sender != nil { - if sender.SessionId == s.PublicId() { - // Don't send message back to sender (can happen if sent to user or room) - return nil - } - if sender.Type == api.RecipientTypeCall { - if room := s.GetRoom(); room == nil || !room.IsSessionInCall(s) { - // Session is not in call, so discard. - return nil - } - } - } - } - case "event": - if msg.Message.Event.Target == "room" || msg.Message.Event.Target == "participants" { - // Can happen mostly during tests where an older room async message - // could be received by a subscriber that joined after it was sent. - if joined := s.getRoomJoinTime(); joined.IsZero() || msg.SendTime.Before(joined) { - s.logger.Printf("Message %+v was sent on %s before session %s join room on %s, ignoring", msg.Message, msg.SendTime, s.publicId, joined) - return nil - } - } - } - - return msg.Message - default: - s.logger.Printf("Received async message with unsupported type %s: %+v", msg.Type, msg) - return nil - } -} - -func (s *ClientSession) NotifySessionResumed(client ClientWithSession) { - s.mu.Lock() - if len(s.pendingClientMessages) == 0 { - s.mu.Unlock() - if room := s.GetRoom(); room != nil { - room.NotifySessionResumed(s) - } - return - } - - messages := s.pendingClientMessages - hasPendingParticipantsUpdate := s.hasPendingParticipantsUpdate - s.pendingClientMessages = nil - s.hasPendingChat = false - s.hasPendingParticipantsUpdate = false - s.mu.Unlock() - - s.logger.Printf("Send %d pending messages to session %s", len(messages), s.PublicId()) - // Send through session to handle connection interruptions. - s.SendMessages(messages) - - if !hasPendingParticipantsUpdate { - // Only need to send initial participants list update if none was part of the pending messages. - if room := s.GetRoom(); room != nil { - room.NotifySessionResumed(s) - } - } -} - -func (s *ClientSession) AddVirtualSession(session *VirtualSession) { - s.mu.Lock() - if s.virtualSessions == nil { - s.virtualSessions = make(map[*VirtualSession]bool) - } - s.virtualSessions[session] = true - s.mu.Unlock() -} - -func (s *ClientSession) RemoveVirtualSession(session *VirtualSession) { - s.mu.Lock() - delete(s.virtualSessions, session) - s.mu.Unlock() -} - -func (s *ClientSession) GetVirtualSessions() []*VirtualSession { - s.mu.Lock() - defer s.mu.Unlock() - - result := make([]*VirtualSession, 0, len(s.virtualSessions)) - for session := range s.virtualSessions { - result = append(result, session) - } - return result -} - -func (s *ClientSession) HandleResponse(id string, handler ResponseHandlerFunc) { - s.responseHandlersLock.Lock() - defer s.responseHandlersLock.Unlock() - - if s.responseHandlers == nil { - s.responseHandlers = make(map[string]ResponseHandlerFunc) - } - - s.responseHandlers[id] = handler -} - -func (s *ClientSession) ClearResponseHandler(id string) { - s.responseHandlersLock.Lock() - defer s.responseHandlersLock.Unlock() - - delete(s.responseHandlers, id) -} - -func (s *ClientSession) ProcessResponse(message *api.ClientMessage) bool { - id := message.Id - if id == "" { - return false - } - - s.responseHandlersLock.Lock() - cb, found := s.responseHandlers[id] - defer s.responseHandlersLock.Unlock() - - if !found { - return false - } - - return cb(message) -} diff --git a/server/clientsession_test.go b/server/clientsession_test.go deleted file mode 100644 index 6152a71..0000000 --- a/server/clientsession_test.go +++ /dev/null @@ -1,919 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2019 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -func TestBandwidth_Client(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - mcu := test.NewSFU(t) - require.NoError(mcu.Start(ctx)) - defer mcu.Stop() - - hub.SetMcu(mcu) - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - defer client.CloseWithBye() - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event. - client.RunUntilJoined(ctx, hello.Hello) - - // Client may not send an offer with audio and video. - bitrate := api.BandwidthFromBits(10000) - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "54321", - RoomType: "video", - Bitrate: bitrate, - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - })) - - require.True(client.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo)) - - pub := mcu.GetPublisher(hello.Hello.SessionId) - require.NotNil(pub) - assert.Equal(bitrate, pub.Settings().Bitrate) -} - -func TestBandwidth_Backend(t *testing.T) { - t.Parallel() - - streamTypes := []sfu.StreamType{ - sfu.StreamTypeVideo, - sfu.StreamTypeScreen, - } - - for _, streamType := range streamTypes { - t.Run(string(streamType), func(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - hub, _, _, server := CreateHubWithMultipleBackendsForTest(t) - - u, err := url.Parse(server.URL + "/one") - require.NoError(err) - backend := hub.backend.GetBackend(u) - require.NotNil(backend, "Could not get backend") - - backend.SetMaxScreenBitrate(1000) - backend.SetMaxStreamBitrate(2000) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - mcu := test.NewSFU(t) - require.NoError(mcu.Start(ctx)) - defer mcu.Stop() - - hub.SetMcu(mcu) - - client := NewTestClient(t, server, hub) - defer client.CloseWithBye() - - params := TestBackendClientAuthParams{ - UserId: testDefaultUserId, - } - require.NoError(client.SendHelloParams(server.URL+"/one", api.HelloVersionV1, "client", nil, params)) - - hello := MustSucceed1(t, client.RunUntilHello, ctx) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event. - require.True(client.RunUntilJoined(ctx, hello.Hello)) - - // Client may not send an offer with audio and video. - bitrate := api.BandwidthFromBits(10000) - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "54321", - RoomType: string(streamType), - Bitrate: bitrate, - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - })) - - require.True(client.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo)) - - pub := mcu.GetPublisher(hello.Hello.SessionId) - require.NotNil(pub, "Could not find publisher") - - var expectBitrate api.Bandwidth - if streamType == sfu.StreamTypeVideo { - expectBitrate = backend.MaxStreamBitrate() - } else { - expectBitrate = backend.MaxScreenBitrate() - } - assert.Equal(expectBitrate, pub.Settings().Bitrate) - }) - } -} - -func TestFeatureChatRelay(t *testing.T) { - t.Parallel() - - testFunc := func(feature bool) func(t *testing.T) { - return func(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - client := NewTestClient(t, server, hub) - defer client.CloseWithBye() - var features []string - if feature { - features = append(features, api.ClientFeatureChatRelay) - } - require.NoError(client.SendHelloClientWithFeatures(testDefaultUserId, features)) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - hello := MustSucceed1(t, client.RunUntilHello, ctx) - - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client.RunUntilJoined(ctx, hello.Hello) - - room := hub.getRoom(roomId) - require.NotNil(room) - - chatComment := api.StringMap{ - "foo": "bar", - "baz": true, - "lala": map[string]any{ - "one": "eins", - }, - "token": roomId, - } - message := api.StringMap{ - "type": "chat", - "chat": api.StringMap{ - "refresh": true, - "comment": chatComment, - }, - } - data, err := json.Marshal(message) - require.NoError(err) - - // Simulate request from the backend. - room.processAsyncMessage(&events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "message", - Message: &talk.BackendRoomMessageRequest{ - Data: data, - }, - }, - }) - - if msg, ok := client.RunUntilRoomMessage(ctx); ok { - assert.Equal(roomId, msg.RoomId) - var data api.StringMap - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - if feature { - assert.EqualValues(chatComment, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - } else { - assert.Equal(true, chat["refresh"]) - _, found := chat["comment"] - assert.False(found, "the comment should not be included") - } - } - } - } - - chatComment2 := api.StringMap{ - "hello": "world", - } - message2 := api.StringMap{ - "type": "chat", - "chat": api.StringMap{ - "refresh": true, - "comments": []api.StringMap{ - chatComment, - chatComment2, - }, - }, - } - data2, err := json.Marshal(message2) - require.NoError(err) - - // Simulate request from the backend. - room.processAsyncMessage(&events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "message", - Message: &talk.BackendRoomMessageRequest{ - Data: data2, - }, - }, - }) - - if msg, ok := client.RunUntilRoomMessage(ctx); ok { - assert.Equal(roomId, msg.RoomId) - var data api.StringMap - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - if feature { - assert.EqualValues(chatComment, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - - // A second message with the second comment will be sent - if msg, ok := client.RunUntilRoomMessage(ctx); ok { - assert.Equal(roomId, msg.RoomId) - - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - assert.EqualValues(chatComment2, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - } - } - } - } else { - // Only a single refresh will be sent - assert.Equal(true, chat["refresh"]) - _, found := chat["comment"] - assert.False(found, "the comment should not be included") - - ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel2() - - client.RunUntilErrorIs(ctx2, context.DeadlineExceeded) - } - } - } - } - } - } - - t.Run("without-chat-relay", testFunc(false)) // nolint:paralleltest - t.Run("with-chat-relay", testFunc(true)) // nolint:paralleltest -} - -func TestFeatureChatRelayFederation(t *testing.T) { - t.Parallel() - - var testFunc = func(feature bool) func(t *testing.T) { - return func(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - hub1, hub2, server1, server2 := CreateClusteredHubsForTest(t) - - localFeatures := []string{ - api.ClientFeatureChatRelay, - } - var federatedFeatures []string - if feature { - federatedFeatures = append(federatedFeatures, api.ClientFeatureChatRelay) - } - - client1 := NewTestClient(t, server1, hub1) - defer client1.CloseWithBye() - require.NoError(client1.SendHelloClientWithFeatures(testDefaultUserId+"1", localFeatures)) - - client2 := NewTestClient(t, server2, hub2) - defer client2.CloseWithBye() - require.NoError(client2.SendHelloClientWithFeatures(testDefaultUserId+"2", federatedFeatures)) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - hello1 := MustSucceed1(t, client1.RunUntilHello, ctx) - hello2 := MustSucceed1(t, client2.RunUntilHello, ctx) - - roomId := "test-room" - federatedRoomId := roomId + "@federated" - room1 := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, room1.Room.RoomId) - - client1.RunUntilJoined(ctx, hello1.Hello) - - now := time.Now() - userdata := api.StringMap{ - "displayname": "Federated user", - "actorType": "federated_users", - "actorId": "the-federated-user-id", - } - token, err := client1.CreateHelloV2TokenWithUserdata(testDefaultUserId+"2", now, now.Add(time.Minute), userdata) - require.NoError(err) - - msg := &api.ClientMessage{ - Id: "join-room-fed", - Type: "room", - Room: &api.RoomClientMessage{ - RoomId: federatedRoomId, - SessionId: api.RoomSessionId(fmt.Sprintf("%s-%s", federatedRoomId, hello2.Hello.SessionId)), - Federation: &api.RoomFederationMessage{ - SignalingUrl: server1.URL, - NextcloudUrl: server1.URL, - RoomId: roomId, - Token: token, - }, - }, - } - require.NoError(client2.WriteJSON(msg)) - - if message, ok := client2.RunUntilMessage(ctx); ok { - assert.Equal(msg.Id, message.Id) - require.Equal("room", message.Type) - require.Equal(federatedRoomId, message.Room.RoomId) - } - - // The client1 will see the remote session id for client2. - var remoteSessionId api.PublicSessionId - if message, ok := client1.RunUntilMessage(ctx); ok { - client1.checkSingleMessageJoined(message) - evt := message.Event.Join[0] - remoteSessionId = evt.SessionId - assert.NotEqual(hello2.Hello.SessionId, remoteSessionId) - assert.Equal(hello2.Hello.UserId, evt.UserId) - assert.True(evt.Federated) - } - - // The client2 will see its own session id, not the one from the remote server. - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) - - room := hub1.getRoom(roomId) - require.NotNil(room) - - chatComment := map[string]any{ - "token": roomId, - "actorId": hello1.Hello.UserId, - "actorType": "users", - "lastEditActorId": hello1.Hello.UserId, - "lastEditActorType": "users", - "parent": map[string]any{ - "actorId": hello1.Hello.UserId, - "actorType": "users", - "lastEditActorId": hello1.Hello.UserId, - "lastEditActorType": "users", - }, - "messageParameters": map[string]map[string]any{ - "mention-local-user": { - "type": "user", - "id": hello1.Hello.UserId, - "name": "User 1", - }, - "mention-remote-user": { - "type": "user", - "id": hello2.Hello.UserId, - "name": "User 2", - "mention-id": "federated_user/" + hello2.Hello.UserId + "@" + getCloudUrl(server2.URL), - "server": server2.URL, - }, - "mention-call": { - "type": "call", - "id": roomId, - }, - }, - } - federatedChatComment := map[string]any{ - "token": federatedRoomId, - "actorId": hello1.Hello.UserId + "@" + getCloudUrl(server1.URL), - "actorType": "federated_users", - "lastEditActorId": hello1.Hello.UserId + "@" + getCloudUrl(server1.URL), - "lastEditActorType": "federated_users", - "parent": map[string]any{ - "actorId": hello1.Hello.UserId + "@" + getCloudUrl(server1.URL), - "actorType": "federated_users", - "lastEditActorId": hello1.Hello.UserId + "@" + getCloudUrl(server1.URL), - "lastEditActorType": "federated_users", - }, - "messageParameters": map[string]map[string]any{ - "mention-local-user": { - "type": "user", - "id": hello1.Hello.UserId, - "mention-id": hello1.Hello.UserId, - "name": "User 1", - "server": server1.URL, - }, - "mention-remote-user": { - "type": "user", - "id": hello2.Hello.UserId, - "name": "User 2", - "mention-id": "federated_user/" + hello2.Hello.UserId + "@" + getCloudUrl(server2.URL), - }, - "mention-call": { - "type": "call", - "id": federatedRoomId, - }, - }, - } - message := api.StringMap{ - "type": "chat", - "chat": api.StringMap{ - "refresh": true, - "comment": chatComment, - }, - } - data, err := json.Marshal(message) - require.NoError(err) - - // Simulate request from the backend. - room.processAsyncMessage(&events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "message", - Message: &talk.BackendRoomMessageRequest{ - Data: data, - }, - }, - }) - - // The first client will receive the message for the local room (always including the actual message). - if msg, ok := client1.RunUntilRoomMessage(ctx); ok { - assert.Equal(roomId, msg.RoomId) - var data api.StringMap - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - AssertEqualSerialized(t, chatComment, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - } - } - } - - // The second client will receive the message from the federated room (either as refresh or with the message). - if msg, ok := client2.RunUntilRoomMessage(ctx); ok { - assert.Equal(federatedRoomId, msg.RoomId) - var data api.StringMap - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - if feature { - AssertEqualSerialized(t, federatedChatComment, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - } else { - assert.Equal(true, chat["refresh"]) - _, found := chat["comment"] - assert.False(found, "the comment should not be included") - } - } - } - } - - chatComment2 := api.StringMap{ - "hello": "world", - } - message2 := api.StringMap{ - "type": "chat", - "chat": api.StringMap{ - "refresh": true, - "comments": []api.StringMap{ - chatComment, - chatComment2, - }, - }, - } - data2, err := json.Marshal(message2) - require.NoError(err) - - // Simulate request from the backend. - room.processAsyncMessage(&events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "message", - Message: &talk.BackendRoomMessageRequest{ - Data: data2, - }, - }, - }) - - // The first client will receive the message for the local room (always including the actual message). - if msg, ok := client1.RunUntilRoomMessage(ctx); ok { - assert.Equal(roomId, msg.RoomId) - var data api.StringMap - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - AssertEqualSerialized(t, chatComment, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - } - } - } - // A second message with the second comment will be sent - if msg, ok := client1.RunUntilRoomMessage(ctx); ok { - assert.Equal(roomId, msg.RoomId) - var data api.StringMap - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - assert.EqualValues(chatComment2, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - } - } - } - - // The second client will receive the message from the federated room (either as refresh or with the message). - if msg, ok := client2.RunUntilRoomMessage(ctx); ok { - assert.Equal(federatedRoomId, msg.RoomId) - var data api.StringMap - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - if feature { - AssertEqualSerialized(t, federatedChatComment, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - - // A second message with the second comment will be sent - if msg, ok := client2.RunUntilRoomMessage(ctx); ok { - assert.Equal(federatedRoomId, msg.RoomId) - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - assert.EqualValues(chatComment2, chat["comment"]) - _, found := chat["refresh"] - assert.False(found, "refresh should not be included") - } - } - } - } else { - // Only a single refresh will be sent - assert.Equal(true, chat["refresh"]) - _, found := chat["comment"] - assert.False(found, "the comment should not be included") - - ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel2() - - client2.RunUntilErrorIs(ctx2, context.DeadlineExceeded) - } - } - } - } - } - } - - t.Run("without-chat-relay", testFunc(false)) // nolint:paralleltest - t.Run("with-chat-relay", testFunc(true)) // nolint:paralleltest -} - -func TestPermissionHideDisplayNames(t *testing.T) { - t.Parallel() - - testFunc := func(permission bool) func(t *testing.T) { - return func(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - defer client.CloseWithBye() - - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client.RunUntilJoined(ctx, hello.Hello) - - room := hub.getRoom(roomId) - require.NotNil(room) - - if permission { - session := hub.GetSessionByPublicId(hello.Hello.SessionId).(*ClientSession) - require.NotNil(session, "Session %s does not exist", hello.Hello.SessionId) - - // Client may not receive display names. - session.SetPermissions([]api.Permission{api.PERMISSION_HIDE_DISPLAYNAMES}) - } - - chatComment := api.StringMap{ - "actorDisplayName": "John Doe", - "baz": true, - "lala": map[string]any{ - "one": "eins", - }, - } - message := api.StringMap{ - "type": "chat", - "chat": api.StringMap{ - "comment": chatComment, - }, - } - data, err := json.Marshal(message) - require.NoError(err) - - // Simulate request from the backend. - room.processAsyncMessage(&events.AsyncMessage{ - Type: "room", - Room: &talk.BackendServerRoomRequest{ - Type: "message", - Message: &talk.BackendRoomMessageRequest{ - Data: data, - }, - }, - }) - - if msg, ok := client.RunUntilRoomMessage(ctx); ok { - assert.Equal(roomId, msg.RoomId) - var data api.StringMap - if err := json.Unmarshal(msg.Data, &data); assert.NoError(err) { - assert.Equal("chat", data["type"], "invalid type entry in %+v", data) - if chat, found := api.GetStringMapEntry[map[string]any](data, "chat"); assert.True(found, "chat entry is missing in %+v", data) { - comment, found := chat["comment"] - if assert.True(found, "comment is missing in %+v", chat) { - if permission { - displayName, found := comment.(map[string]any)["actorDisplayName"] - assert.True(!found || displayName == "", "the display name should not be included in %+v", comment) - } else { - displayName, found := comment.(map[string]any)["actorDisplayName"] - assert.True(found && displayName != "", "the display name should be included in %+v", comment) - } - } - } - } - - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - defer client2.CloseWithBye() - - roomMsg2 := MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg2.Room.RoomId) - - client.RunUntilJoined(ctx, hello2.Hello) - client2.RunUntilJoined(ctx, hello.Hello, hello2.Hello) - - recipient1 := api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - } - data1 := api.StringMap{ - "type": "nickChanged", - "message": "from-1-to-2", - } - client2.SendMessage(recipient1, data1) // nolint - if permission { - ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel2() - - client.RunUntilErrorIs(ctx2, context.DeadlineExceeded) - } else { - var payload2 api.StringMap - if ok := checkReceiveClientMessage(ctx, t, client, "session", hello2.Hello, &payload2); ok { - assert.Equal(data1, payload2) - } - } - } - } - } - - t.Run("without-hide-displaynames", testFunc(false)) // nolint:paralleltest - t.Run("with-hide-displaynames", testFunc(true)) // nolint:paralleltest -} - -func Test_ClientSessionPublisherEvents(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - mcu := test.NewSFU(t) - require.NoError(mcu.Start(ctx)) - defer mcu.Stop() - - hub.SetMcu(mcu) - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - defer client.CloseWithBye() - - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client.RunUntilJoined(ctx, hello.Hello) - - room := hub.getRoom(roomId) - require.NotNil(room) - - session := hub.GetSessionByPublicId(hello.Hello.SessionId).(*ClientSession) - require.NotNil(session, "Session %s does not exist", hello.Hello.SessionId) - - require.NoError(client.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "54321", - RoomType: string(sfu.StreamTypeVideo), - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - })) - - require.True(client.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo)) - - pub := mcu.GetPublisher(hello.Hello.SessionId) - require.NotNil(pub) - - assert.Equal(pub, session.GetPublisher(sfu.StreamTypeVideo)) - session.OnIceCandidate(pub, "test-candidate") - - if message, ok := client.RunUntilMessage(ctx); ok { - assert.Equal("message", message.Type) - if msg := message.Message; assert.NotNil(msg) { - if sender := msg.Sender; assert.NotNil(sender) { - assert.Equal("session", sender.Type) - assert.Equal(hello.Hello.SessionId, sender.SessionId) - } - var ao api.AnswerOfferMessage - if assert.NoError(json.Unmarshal(msg.Data, &ao)) { - assert.Equal(hello.Hello.SessionId, ao.From) - assert.Equal(hello.Hello.SessionId, ao.To) - assert.Equal("candidate", ao.Type) - assert.EqualValues(sfu.StreamTypeVideo, ao.RoomType) - assert.Equal("test-candidate", ao.Payload["candidate"]) - } - } - } - - // No-op - session.OnIceCompleted(pub) - - session.PublisherClosed(pub) - assert.Nil(session.GetPublisher(sfu.StreamTypeVideo)) -} - -func Test_ClientSessionSubscriberEvents(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - hub.allowSubscribeAnyStream = true - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - mcu := test.NewSFU(t) - require.NoError(mcu.Start(ctx)) - defer mcu.Stop() - - hub.SetMcu(mcu) - - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - defer client1.CloseWithBye() - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - defer client2.CloseWithBye() - - roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) - - room := hub.getRoom(roomId) - require.NotNil(room) - - session1 := hub.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession) - require.NotNil(session1, "Session %s does not exist", hello1.Hello.SessionId) - session2 := hub.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) - require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) - - require.NoError(client1.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "offer", - Sid: "54321", - RoomType: string(sfu.StreamTypeVideo), - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - })) - - require.True(client1.RunUntilAnswer(ctx, mock.MockSdpAnswerAudioAndVideo)) - - require.NoError(client2.SendMessage(api.MessageClientMessageRecipient{ - Type: "session", - SessionId: hello1.Hello.SessionId, - }, api.MessageClientMessageData{ - Type: "requestoffer", - Sid: "54321", - RoomType: string(sfu.StreamTypeVideo), - })) - - require.True(client2.RunUntilOffer(ctx, mock.MockSdpOfferAudioAndVideo)) - - sub := mcu.GetSubscriber(hello1.Hello.SessionId, sfu.StreamTypeVideo) - require.NotNil(sub) - - assert.Equal(sub, session2.GetSubscriber(hello1.Hello.SessionId, sfu.StreamTypeVideo)) - session2.OnIceCandidate(sub, "test-candidate") - - if message, ok := client2.RunUntilMessage(ctx); ok { - assert.Equal("message", message.Type) - if msg := message.Message; assert.NotNil(msg) { - if sender := msg.Sender; assert.NotNil(sender) { - assert.Equal("session", sender.Type) - assert.Equal(hello1.Hello.SessionId, sender.SessionId) - } - var ao api.AnswerOfferMessage - if assert.NoError(json.Unmarshal(msg.Data, &ao)) { - assert.Equal(hello1.Hello.SessionId, ao.From) - assert.Equal(hello2.Hello.SessionId, ao.To) - assert.Equal("candidate", ao.Type) - assert.EqualValues(sfu.StreamTypeVideo, ao.RoomType) - assert.Equal("test-candidate", ao.Payload["candidate"]) - } - } - } - - // No-op - session2.OnIceCompleted(sub) - - session2.OnUpdateOffer(sub, api.StringMap{ - "type": "offer", - "sdp": mock.MockSdpOfferAudioOnly, - }) - - require.True(client2.RunUntilOffer(ctx, mock.MockSdpOfferAudioOnly)) - - session2.SubscriberClosed(sub) - assert.Nil(session2.GetSubscriber(hello1.Hello.SessionId, sfu.StreamTypeVideo)) -} diff --git a/server/hub_client.go b/server/hub_client.go deleted file mode 100644 index 3a7b973..0000000 --- a/server/hub_client.go +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "context" - "strings" - "sync/atomic" - "time" - - "github.com/gorilla/websocket" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/client" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" -) - -var ( - InvalidFormat = client.InvalidFormat -) - -func init() { - RegisterClientStats() -} - -type HubClient struct { - client.Client - - hub *Hub - session atomic.Pointer[Session] -} - -func NewHubClient(ctx context.Context, conn *websocket.Conn, remoteAddress string, agent string, hub *Hub) (*HubClient, error) { - remoteAddress = strings.TrimSpace(remoteAddress) - if remoteAddress == "" { - remoteAddress = "unknown remote address" - } - agent = strings.TrimSpace(agent) - if agent == "" { - agent = "unknown user agent" - } - - client := &HubClient{ - hub: hub, - } - client.SetConn(ctx, conn, remoteAddress, agent, true, client) - return client, nil -} - -func (c *HubClient) OnLookupCountry(addr string) geoip.Country { - return c.hub.LookupCountry(addr) -} - -func (c *HubClient) OnClosed() { - c.hub.processUnregister(c) -} - -func (c *HubClient) OnMessageReceived(data []byte) { - c.hub.processMessage(c, data) -} - -func (c *HubClient) OnRTTReceived(rtt time.Duration) { - // Ignore -} - -func (c *HubClient) CloseSession() { - if session := c.GetSession(); session != nil { - session.Close() - } -} - -func (c *HubClient) IsAuthenticated() bool { - return c.GetSession() != nil -} - -func (c *HubClient) GetSession() Session { - session := c.session.Load() - if session == nil { - return nil - } - - return *session -} - -func (c *HubClient) SetSession(session Session) { - if session == nil { - c.session.Store(nil) - } else { - c.session.Store(&session) - } -} - -func (c *HubClient) GetSessionId() api.PublicSessionId { - session := c.GetSession() - if session == nil { - return "" - } - - return session.PublicId() -} - -func (c *HubClient) IsInRoom(id string) bool { - session := c.GetSession() - if session == nil { - return false - } - - return session.IsInRoom(id) -} diff --git a/server/hub_transient_data_test.go b/server/hub_transient_data_test.go deleted file mode 100644 index ab6e4dd..0000000 --- a/server/hub_transient_data_test.go +++ /dev/null @@ -1,383 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "context" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" -) - -func Test_TransientMessages(t *testing.T) { - t.Parallel() - for _, subtest := range clusteredTests { - t.Run(subtest, func(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - var hub1 *Hub - var hub2 *Hub - var server1 *httptest.Server - var server2 *httptest.Server - if isLocalTest(t) { - hub1, _, _, server1 = CreateHubForTest(t) - - hub2 = hub1 - server2 = server1 - } else { - hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) - } - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - - require.NoError(client1.SetTransientData("foo", "bar", 0)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageError(t, msg, "not_in_room") - } - - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // Give message processing some time. - time.Sleep(10 * time.Millisecond) - - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) - - session1 := hub1.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession) - require.NotNil(session1, "Session %s does not exist", hello1.Hello.SessionId) - session2 := hub2.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) - require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) - - // Client 1 may modify transient data. - session1.SetPermissions([]api.Permission{api.PERMISSION_TRANSIENT_DATA}) - // Client 2 may not modify transient data. - session2.SetPermissions([]api.Permission{}) - - require.NoError(client2.SetTransientData("foo", "bar", 0)) - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageError(t, msg, "not_allowed") - } - - require.NoError(client1.SetTransientData("foo", "bar", 0)) - - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "foo", "bar", nil) - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "foo", "bar", nil) - } - - require.NoError(client2.RemoveTransientData("foo")) - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageError(t, msg, "not_allowed") - } - - // Setting the same value is ignored by the server. - require.NoError(client1.SetTransientData("foo", "bar", 0)) - ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel2() - - client1.RunUntilErrorIs(ctx2, context.DeadlineExceeded) - - data := map[string]any{ - "hello": "world", - } - require.NoError(client1.SetTransientData("foo", data, 0)) - - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "foo", data, "bar") - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "foo", data, "bar") - } - - require.NoError(client1.RemoveTransientData("foo")) - - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientRemove(t, msg, "foo", data) - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientRemove(t, msg, "foo", data) - } - - // Removing a non-existing key is ignored by the server. - require.NoError(client1.RemoveTransientData("foo")) - ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel3() - - client1.RunUntilErrorIs(ctx3, context.DeadlineExceeded) - - ttl := 200 * time.Millisecond - require.NoError(client1.SetTransientData("abc", data, ttl)) - setAt := time.Now() - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, "abc", data, nil) - } - - client1.CloseWithBye() - require.NoError(client1.WaitForClientRemoved(ctx)) - client2.RunUntilLeft(ctx, hello1.Hello) - - client3, hello3 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"3") - roomMsg = MustSucceed2(t, client3.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client2.RunUntilJoined(ctx, hello3.Hello) - - _, ignored, ok := client3.RunUntilJoinedAndReturn(ctx, hello2.Hello, hello3.Hello) - require.True(ok) - - var msg *api.ServerMessage - if len(ignored) == 0 { - msg = MustSucceed1(t, client3.RunUntilMessage, ctx) - } else if len(ignored) == 1 { - msg = ignored[0] - } else { - require.LessOrEqual(len(ignored), 1, "Received too many messages: %+v", ignored) - } - - checkMessageTransientInitial(t, msg, api.StringMap{ - "abc": data, - }) - - client4, hello4 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"4") - roomMsg = MustSucceed2(t, client4.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client2.RunUntilJoined(ctx, hello4.Hello) - client3.RunUntilJoined(ctx, hello4.Hello) - - _, ignored, ok = client4.RunUntilJoinedAndReturn(ctx, hello2.Hello, hello3.Hello, hello4.Hello) - require.True(ok) - - if len(ignored) == 0 { - msg = MustSucceed1(t, client4.RunUntilMessage, ctx) - } else if len(ignored) == 1 { - msg = ignored[0] - } else { - require.LessOrEqual(len(ignored), 1, "Received too many messages: %+v", ignored) - } - - checkMessageTransientInitial(t, msg, api.StringMap{ - "abc": data, - }) - - delta := time.Until(setAt.Add(ttl)) - if assert.Greater(delta, time.Duration(0), "test runner too slow?") { - time.Sleep(delta) - if msg, ok = client2.RunUntilMessage(ctx); ok { - checkMessageTransientRemove(t, msg, "abc", data) - } - } - }) - } -} - -func Test_TransientSessionData(t *testing.T) { - t.Parallel() - for _, subtest := range clusteredTests { - t.Run(subtest, func(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - var hub1 *Hub - var hub2 *Hub - var server1 *httptest.Server - var server2 *httptest.Server - if isLocalTest(t) { - hub1, _, _, server1 = CreateHubForTest(t) - - hub2 = hub1 - server2 = server1 - } else { - hub1, hub2, server1, server2 = CreateClusteredHubsForTest(t) - } - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client1, hello1 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server2, hub2, testDefaultUserId+"2") - - roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) - - sessionKey1 := "sd:" + string(hello1.Hello.SessionId) - sessionKey2 := "sd:" + string(hello2.Hello.SessionId) - require.NotEqual(sessionKey1, sessionKey2) - - require.NoError(client1.SetTransientData(sessionKey2, "foo", 0)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageError(t, msg, "not_allowed") - } - require.NoError(client2.SetTransientData(sessionKey1, "bar", 0)) - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageError(t, msg, "not_allowed") - } - - require.NoError(client1.SetTransientData(sessionKey1, "foo", 0)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey1, "foo", nil) - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey1, "foo", nil) - } - - require.NoError(client2.RemoveTransientData(sessionKey1)) - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageError(t, msg, "not_allowed") - } - - require.NoError(client2.SetTransientData(sessionKey2, "bar", 0)) - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey2, "bar", nil) - } - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey2, "bar", nil) - } - - client1.CloseWithBye() - assert.NoError(client1.WaitForClientRemoved(ctx)) - - var messages []*api.ServerMessage - for range 2 { - if msg, ok := client2.RunUntilMessage(ctx); ok { - messages = append(messages, msg) - } - } - if assert.Len(messages, 2) { - if messages[0].Type == "transient" { - messages[0], messages[1] = messages[1], messages[0] - } - client2.checkMessageRoomLeaveSession(messages[0], hello1.Hello.SessionId) - checkMessageTransientRemove(t, messages[1], sessionKey1, "foo") - } - - client3, hello3 := NewTestClientWithHello(ctx, t, server1, hub1, testDefaultUserId+"3") - roomMsg = MustSucceed2(t, client3.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - _, ignored, ok := client3.RunUntilJoinedAndReturn(ctx, hello2.Hello, hello3.Hello) - require.True(ok) - - var msg *api.ServerMessage - if len(ignored) == 0 { - msg = MustSucceed1(t, client3.RunUntilMessage, ctx) - } else if len(ignored) == 1 { - msg = ignored[0] - } else { - require.LessOrEqual(len(ignored), 1, "Received too many messages: %+v", ignored) - } - - checkMessageTransientInitial(t, msg, api.StringMap{ - sessionKey2: "bar", - }) - - client2.CloseWithBye() - assert.NoError(client2.WaitForClientRemoved(ctx)) - - messages = nil - for range 2 { - if msg, ok := client3.RunUntilMessage(ctx); ok { - messages = append(messages, msg) - } - } - if assert.Len(messages, 2) { - if messages[0].Type == "transient" { - messages[0], messages[1] = messages[1], messages[0] - } - client3.checkMessageRoomLeaveSession(messages[0], hello2.Hello.SessionId) - checkMessageTransientRemove(t, messages[1], sessionKey2, "bar") - } - - // Internal clients may set transient data of any session. - client4 := NewTestClient(t, server1, hub1) - defer client4.CloseWithBye() - - require.NoError(client4.SendHelloInternal()) - hello4 := MustSucceed1(t, client4.RunUntilHello, ctx) - roomMsg = MustSucceed2(t, client4.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - _, ignored, ok = client4.RunUntilJoinedAndReturn(ctx, hello3.Hello, hello4.Hello) - require.True(ok) - - if len(ignored) == 0 { - msg = MustSucceed1(t, client4.RunUntilMessage, ctx) - } else if len(ignored) == 1 { - msg = ignored[0] - } else { - require.LessOrEqual(len(ignored), 1, "Received too many messages: %+v", ignored) - } - - if msgInCall, ok := checkMessageParticipantsInCall(t, msg); ok { - if assert.Len(msgInCall.Users, 1) { - assert.Equal(true, msgInCall.Users[0]["internal"]) - assert.EqualValues(hello4.Hello.SessionId, msgInCall.Users[0]["sessionId"]) - assert.EqualValues(FlagInCall|FlagWithAudio, msgInCall.Users[0]["inCall"]) - } - } - - client3.RunUntilJoined(ctx, hello4.Hello) - if msg, ok := client3.RunUntilMessage(ctx); ok { - if msgInCall, ok := checkMessageParticipantsInCall(t, msg); ok { - if assert.Len(msgInCall.Users, 1) { - assert.Equal(true, msgInCall.Users[0]["internal"]) - assert.EqualValues(hello4.Hello.SessionId, msgInCall.Users[0]["sessionId"]) - assert.EqualValues(FlagInCall|FlagWithAudio, msgInCall.Users[0]["inCall"]) - } - } - } - - sessionKey3 := string("sd:" + hello3.Hello.SessionId) - require.NoError(client4.SetTransientData(sessionKey3, "baz", 0)) - if msg, ok := client4.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey3, "baz", nil) - } - - if msg, ok := client3.RunUntilMessage(ctx); ok { - checkMessageTransientSet(t, msg, sessionKey3, "baz", nil) - } - }) - } -} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..e372bd9 --- /dev/null +++ b/server/main.go @@ -0,0 +1,432 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package main + +import ( + "context" + "crypto/tls" + "errors" + "flag" + "fmt" + "log" + "net" + "net/http" + "net/http/pprof" + "os" + "os/signal" + "runtime" + runtimepprof "runtime/pprof" + "strings" + "sync" + "syscall" + "time" + + "github.com/dlintw/goconf" + "github.com/gorilla/mux" + "github.com/nats-io/nats.go" + + signaling "github.com/strukturag/nextcloud-spreed-signaling" +) + +var ( + version = "unreleased" + + configFlag = flag.String("config", "server.conf", "config file to use") + + cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") + + memprofile = flag.String("memprofile", "", "write memory profile to file") + + showVersion = flag.Bool("version", false, "show version and quit") +) + +const ( + defaultReadTimeout = 15 + defaultWriteTimeout = 30 + + initialMcuRetry = time.Second + maxMcuRetry = time.Second * 16 + + dnsMonitorInterval = time.Second +) + +func createListener(addr string) (net.Listener, error) { + if addr[0] == '/' { + os.Remove(addr) + return net.Listen("unix", addr) + } + + return net.Listen("tcp", addr) +} + +func createTLSListener(addr string, certFile, keyFile string) (net.Listener, error) { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, err + } + config := tls.Config{ + Certificates: []tls.Certificate{cert}, + } + if addr[0] == '/' { + os.Remove(addr) + return tls.Listen("unix", addr, &config) + } + + return tls.Listen("tcp", addr, &config) +} + +type Listeners struct { + mu sync.Mutex + listeners []net.Listener +} + +func (l *Listeners) Add(listener net.Listener) { + l.mu.Lock() + defer l.mu.Unlock() + + l.listeners = append(l.listeners, listener) +} + +func (l *Listeners) Close() { + l.mu.Lock() + defer l.mu.Unlock() + + for _, listener := range l.listeners { + if err := listener.Close(); err != nil { + log.Printf("Error closing listener %s: %s", listener.Addr(), err) + } + } +} + +func main() { + log.SetFlags(log.Lshortfile) + flag.Parse() + + if *showVersion { + fmt.Printf("nextcloud-spreed-signaling version %s/%s\n", version, runtime.Version()) + os.Exit(0) + } + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt) + signal.Notify(sigChan, syscall.SIGHUP) + signal.Notify(sigChan, syscall.SIGUSR1) + + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal(err) + } + + if err := runtimepprof.StartCPUProfile(f); err != nil { + log.Fatalf("Error writing CPU profile to %s: %s", *cpuprofile, err) + } + log.Printf("Writing CPU profile to %s ...", *cpuprofile) + defer runtimepprof.StopCPUProfile() + } + + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + log.Fatal(err) + } + + defer func() { + log.Printf("Writing Memory profile to %s ...", *memprofile) + runtime.GC() + if err := runtimepprof.WriteHeapProfile(f); err != nil { + log.Printf("Error writing Memory profile to %s: %s", *memprofile, err) + } + }() + } + + log.Printf("Starting up version %s/%s as pid %d", version, runtime.Version(), os.Getpid()) + + config, err := goconf.ReadConfigFile(*configFlag) + if err != nil { + log.Fatal("Could not read configuration: ", err) + } + + cpus := runtime.NumCPU() + runtime.GOMAXPROCS(cpus) + log.Printf("Using a maximum of %d CPUs", cpus) + + signaling.RegisterStats() + + natsUrl, _ := signaling.GetStringOptionWithEnv(config, "nats", "url") + if natsUrl == "" { + natsUrl = nats.DefaultURL + } + + events, err := signaling.NewAsyncEvents(natsUrl) + if err != nil { + log.Fatal("Could not create async events client: ", err) + } + defer events.Close() + + dnsMonitor, err := signaling.NewDnsMonitor(dnsMonitorInterval) + if err != nil { + log.Fatal("Could not create DNS monitor: ", err) + } + if err := dnsMonitor.Start(); err != nil { + log.Fatal("Could not start DNS monitor: ", err) + } + defer dnsMonitor.Stop() + + etcdClient, err := signaling.NewEtcdClient(config, "mcu") + if err != nil { + log.Fatalf("Could not create etcd client: %s", err) + } + defer func() { + if err := etcdClient.Close(); err != nil { + log.Printf("Error while closing etcd client: %s", err) + } + }() + + rpcServer, err := signaling.NewGrpcServer(config, version) + if err != nil { + log.Fatalf("Could not create RPC server: %s", err) + } + go func() { + if err := rpcServer.Run(); err != nil { + log.Fatalf("Could not start RPC server: %s", err) + } + }() + defer rpcServer.Close() + + rpcClients, err := signaling.NewGrpcClients(config, etcdClient, dnsMonitor, version) + if err != nil { + log.Fatalf("Could not create RPC clients: %s", err) + } + defer rpcClients.Close() + + r := mux.NewRouter() + hub, err := signaling.NewHub(config, events, rpcServer, rpcClients, etcdClient, r, version) + if err != nil { + log.Fatal("Could not create hub: ", err) + } + + mcuUrl, _ := signaling.GetStringOptionWithEnv(config, "mcu", "url") + mcuType, _ := config.GetString("mcu", "type") + if mcuType == "" && mcuUrl != "" { + log.Printf("WARNING: Old-style MCU configuration detected with url but no type, defaulting to type %s", signaling.McuTypeJanus) + mcuType = signaling.McuTypeJanus + } else if mcuType == signaling.McuTypeJanus && mcuUrl == "" { + log.Printf("WARNING: Old-style MCU configuration detected with type but no url, disabling") + mcuType = "" + } + + if mcuType != "" { + var mcu signaling.Mcu + mcuRetry := initialMcuRetry + mcuRetryTimer := time.NewTimer(mcuRetry) + mcuTypeLoop: + for { + // Context should be cancelled on signals but need a way to differentiate later. + ctx := context.TODO() + switch mcuType { + case signaling.McuTypeJanus: + mcu, err = signaling.NewMcuJanus(ctx, mcuUrl, config) + signaling.UnregisterProxyMcuStats() + signaling.RegisterJanusMcuStats() + case signaling.McuTypeProxy: + mcu, err = signaling.NewMcuProxy(config, etcdClient, rpcClients, dnsMonitor) + signaling.UnregisterJanusMcuStats() + signaling.RegisterProxyMcuStats() + default: + log.Fatal("Unsupported MCU type: ", mcuType) + } + if err == nil { + err = mcu.Start(ctx) + if err != nil { + log.Printf("Could not create %s MCU: %s", mcuType, err) + } + } + if err == nil { + break + } + + log.Printf("Could not initialize %s MCU (%s) will retry in %s", mcuType, err, mcuRetry) + mcuRetryTimer.Reset(mcuRetry) + select { + case sig := <-sigChan: + switch sig { + case os.Interrupt: + log.Fatalf("Cancelled") + case syscall.SIGHUP: + log.Printf("Received SIGHUP, reloading %s", *configFlag) + if config, err = goconf.ReadConfigFile(*configFlag); err != nil { + log.Printf("Could not read configuration from %s: %s", *configFlag, err) + } else { + mcuUrl, _ = signaling.GetStringOptionWithEnv(config, "mcu", "url") + mcuType, _ = config.GetString("mcu", "type") + if mcuType == "" && mcuUrl != "" { + log.Printf("WARNING: Old-style MCU configuration detected with url but no type, defaulting to type %s", signaling.McuTypeJanus) + mcuType = signaling.McuTypeJanus + } else if mcuType == signaling.McuTypeJanus && mcuUrl == "" { + log.Printf("WARNING: Old-style MCU configuration detected with type but no url, disabling") + mcuType = "" + break mcuTypeLoop + } + } + } + case <-mcuRetryTimer.C: + // Retry connection + mcuRetry = mcuRetry * 2 + if mcuRetry > maxMcuRetry { + mcuRetry = maxMcuRetry + } + } + } + if mcu != nil { + defer mcu.Stop() + + log.Printf("Using %s MCU", mcuType) + hub.SetMcu(mcu) + } + } + + go hub.Run() + defer hub.Stop() + + server, err := signaling.NewBackendServer(config, hub, version) + if err != nil { + log.Fatal("Could not create backend server: ", err) + } + if err := server.Start(r); err != nil { + log.Fatal("Could not start backend server: ", err) + } + + if debug, _ := config.GetBool("app", "debug"); debug { + log.Println("Installing debug handlers in \"/debug/pprof\"") + r.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) + r.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) + r.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) + r.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) + r.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) + for _, profile := range runtimepprof.Profiles() { + name := profile.Name() + r.Handle("/debug/pprof/"+name, pprof.Handler(name)) + } + } + + var listeners Listeners + + if saddr, _ := signaling.GetStringOptionWithEnv(config, "https", "listen"); saddr != "" { + cert, _ := config.GetString("https", "certificate") + key, _ := config.GetString("https", "key") + if cert == "" || key == "" { + log.Fatal("Need a certificate and key for the HTTPS listener") + } + + readTimeout, _ := config.GetInt("https", "readtimeout") + if readTimeout <= 0 { + readTimeout = defaultReadTimeout + } + writeTimeout, _ := config.GetInt("https", "writetimeout") + if writeTimeout <= 0 { + writeTimeout = defaultWriteTimeout + } + for _, address := range strings.Split(saddr, " ") { + go func(address string) { + log.Println("Listening on", address) + listener, err := createTLSListener(address, cert, key) + if err != nil { + log.Fatal("Could not start listening: ", err) + } + srv := &http.Server{ + Handler: r, + + ReadTimeout: time.Duration(readTimeout) * time.Second, + WriteTimeout: time.Duration(writeTimeout) * time.Second, + } + listeners.Add(listener) + if err := srv.Serve(listener); err != nil { + if !hub.IsShutdownScheduled() || !errors.Is(err, net.ErrClosed) { + log.Fatal("Could not start server: ", err) + } + } + }(address) + } + } + + if addr, _ := signaling.GetStringOptionWithEnv(config, "http", "listen"); addr != "" { + readTimeout, _ := config.GetInt("http", "readtimeout") + if readTimeout <= 0 { + readTimeout = defaultReadTimeout + } + writeTimeout, _ := config.GetInt("http", "writetimeout") + if writeTimeout <= 0 { + writeTimeout = defaultWriteTimeout + } + + for _, address := range strings.Split(addr, " ") { + go func(address string) { + log.Println("Listening on", address) + listener, err := createListener(address) + if err != nil { + log.Fatal("Could not start listening: ", err) + } + srv := &http.Server{ + Handler: r, + Addr: addr, + + ReadTimeout: time.Duration(readTimeout) * time.Second, + WriteTimeout: time.Duration(writeTimeout) * time.Second, + } + listeners.Add(listener) + if err := srv.Serve(listener); err != nil { + if !hub.IsShutdownScheduled() || !errors.Is(err, net.ErrClosed) { + log.Fatal("Could not start server: ", err) + } + } + }(address) + } + } + +loop: + for { + select { + case sig := <-sigChan: + switch sig { + case os.Interrupt: + log.Println("Interrupted") + break loop + case syscall.SIGHUP: + log.Printf("Received SIGHUP, reloading %s", *configFlag) + if config, err := goconf.ReadConfigFile(*configFlag); err != nil { + log.Printf("Could not read configuration from %s: %s", *configFlag, err) + } else { + hub.Reload(config) + server.Reload(config) + } + case syscall.SIGUSR1: + log.Printf("Received SIGUSR1, scheduling server to shutdown") + hub.ScheduleShutdown() + listeners.Close() + } + case <-hub.ShutdownChannel(): + log.Printf("All clients disconnected, shutting down") + break loop + } + } +} diff --git a/server/room_stats_prometheus.go b/server/room_stats_prometheus.go deleted file mode 100644 index 098ee25..0000000 --- a/server/room_stats_prometheus.go +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" -) - -var ( - statsRoomSessionsCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "room", - Name: "sessions", - Help: "The current number of sessions in a room", - }, []string{"backend", "room", "clienttype"}) - statsCallSessionsCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "call", - Name: "sessions", - Help: "The current number of sessions in a call", - }, []string{"backend", "room", "clienttype"}) - statsCallSessionsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "call", - Name: "sessions_total", - Help: "The total number of sessions in a call", - }, []string{"backend", "clienttype"}) - statsCallRoomsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "call", - Name: "rooms_total", - Help: "The total number of rooms with an active call", - }, []string{"backend"}) - - roomStats = []prometheus.Collector{ - statsRoomSessionsCurrent, - statsCallSessionsCurrent, - statsCallSessionsTotal, - statsCallRoomsTotal, - } -) - -func RegisterRoomStats() { - metrics.RegisterAll(roomStats...) -} diff --git a/server/room_test.go b/server/room_test.go deleted file mode 100644 index 3944512..0000000 --- a/server/room_test.go +++ /dev/null @@ -1,590 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -func TestRoom_InCallFlag(t *testing.T) { - t.Parallel() - type Testcase struct { - Value any - InCall bool - Valid bool - } - tests := []Testcase{ - {nil, false, false}, - {"a", false, false}, - {true, true, true}, - {false, false, true}, - {0, false, true}, - {FlagDisconnected, false, true}, - {1, true, true}, - {FlagInCall, true, true}, - {2, false, true}, - {FlagWithAudio, false, true}, - {3, true, true}, - {FlagInCall | FlagWithAudio, true, true}, - {4, false, true}, - {FlagWithVideo, false, true}, - {5, true, true}, - {FlagInCall | FlagWithVideo, true, true}, - {1.1, true, true}, - {json.Number("1"), true, true}, - {json.Number("1.1"), false, false}, - } - for _, test := range tests { - inCall, ok := IsInCall(test.Value) - if test.Valid { - assert.True(t, ok, "%+v should be valid", test.Value) - } else { - assert.False(t, ok, "%+v should not be valid", test.Value) - } - assert.Equal(t, test.InCall, inCall, "conversion failed for %+v", test.Value) - } -} - -func TestRoom_Update(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - require := require.New(t) - assert := assert.New(t) - hub, _, router, server := CreateHubForTest(t) - - config, err := getTestConfig(server) - require.NoError(err) - b, err := NewBackendServer(ctx, config, hub, "no-version") - require.NoError(err) - require.NoError(b.Start(router)) - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event. - assert.True(client.RunUntilJoined(ctx, hello.Hello)) - - // Simulate backend request from Nextcloud to update the room. - roomProperties := json.RawMessage("{\"foo\":\"bar\"}") - msg := &talk.BackendServerRoomRequest{ - Type: "update", - Update: &talk.BackendRoomUpdateRequest{ - UserIds: []string{ - testDefaultUserId, - }, - Properties: roomProperties, - }, - } - - data, err := json.Marshal(msg) - require.NoError(err) - res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data) - require.NoError(err) - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - assert.NoError(err) - assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - - // The client receives a roomlist update and a changed room event. The - // ordering is not defined because messages are sent by asynchronous event - // handlers. - message1, _ := client.RunUntilMessage(ctx) - message2, _ := client.RunUntilMessage(ctx) - - if message1.Type != "event" { - checkMessageRoomId(t, message1, roomId) - if msg, ok := checkMessageRoomlistUpdate(t, message2); ok { - assert.Equal(roomId, msg.RoomId) - assert.Equal(string(roomProperties), string(msg.Properties)) - } - } else if msg, ok := checkMessageRoomlistUpdate(t, message1); ok { - assert.Equal(roomId, msg.RoomId) - assert.Equal(string(roomProperties), string(msg.Properties)) - checkMessageRoomId(t, message2, roomId) - } - - // Allow up to 100 milliseconds for asynchronous event processing. - ctx2, cancel2 := context.WithTimeout(ctx, 100*time.Millisecond) - defer cancel2() - -loop: - for { - select { - case <-ctx2.Done(): - break loop - default: - // The internal room has been updated with the new properties. - if room := hub.getRoom(roomId); room == nil { - err = fmt.Errorf("Room %s not found in hub", roomId) - } else if len(room.Properties()) == 0 || !bytes.Equal(room.Properties(), roomProperties) { - err = fmt.Errorf("Expected room properties %s, got %+v", string(roomProperties), room.Properties()) - } else { - err = nil - } - } - if err == nil { - break - } - - time.Sleep(time.Millisecond) - } - - assert.NoError(err) -} - -func TestRoom_Delete(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - require := require.New(t) - assert := assert.New(t) - hub, _, router, server := CreateHubForTest(t) - - config, err := getTestConfig(server) - require.NoError(err) - b, err := NewBackendServer(ctx, config, hub, "no-version") - require.NoError(err) - require.NoError(b.Start(router)) - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event. - assert.True(client.RunUntilJoined(ctx, hello.Hello)) - - // Simulate backend request from Nextcloud to update the room. - msg := &talk.BackendServerRoomRequest{ - Type: "delete", - Delete: &talk.BackendRoomDeleteRequest{ - UserIds: []string{ - testDefaultUserId, - }, - }, - } - - data, err := json.Marshal(msg) - require.NoError(err) - res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data) - require.NoError(err) - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - assert.NoError(err) - assert.Equal(http.StatusOK, res.StatusCode, "Expected successful request, got %s", string(body)) - - // The client is no longer invited to the room and leaves it. The ordering - // of messages is not defined as they get published through events and handled - // by asynchronous channels. - if message1, ok := client.RunUntilMessage(ctx); ok && message1.Type != "event" { - // Ordering should be "leave room", "disinvited". - checkMessageRoomId(t, message1, "") - if message2, ok := client.RunUntilMessage(ctx); ok { - checkMessageRoomlistDisinvite(t, message2) - } - } else { - // Ordering should be "disinvited", "leave room". - checkMessageRoomlistDisinvite(t, message1) - // The connection should get closed after the "disinvited". - // However due to the asynchronous processing, the "leave room" message might be received before. - if message2, ok := client.RunUntilMessageOrClosed(ctx); ok && message2 != nil { - checkMessageRoomId(t, message2, "") - client.RunUntilClosed(ctx) - } - } - - // Allow up to 100 milliseconds for asynchronous event processing. - ctx2, cancel2 := context.WithTimeout(ctx, 100*time.Millisecond) - defer cancel2() - -loop: - for { - select { - case <-ctx2.Done(): - err = ctx2.Err() - break loop - default: - // The internal room has been updated with the new properties. - hub.ru.Lock() - _, found := hub.rooms[roomId] - hub.ru.Unlock() - - if found { - err = fmt.Errorf("Room %s still found in hub", roomId) - } else { - err = nil - } - } - if err == nil { - break - } - - time.Sleep(time.Millisecond) - } - - assert.NoError(err) -} - -func TestRoom_RoomJoinFeatures(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - require := require.New(t) - assert := assert.New(t) - hub, _, router, server := CreateHubForTest(t) - - config, err := getTestConfig(server) - require.NoError(err) - b, err := NewBackendServer(ctx, config, hub, "no-version") - require.NoError(err) - require.NoError(b.Start(router)) - - client := NewTestClient(t, server, hub) - defer client.CloseWithBye() - - features := []string{"one", "two", "three"} - require.NoError(client.SendHelloClientWithFeatures(testDefaultUserId, features)) - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - hello := MustSucceed1(t, client.RunUntilHello, ctx) - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - if message, ok := client.RunUntilMessage(ctx); ok { - if client.checkMessageJoinedSession(message, hello.Hello.SessionId, testDefaultUserId) { - assert.EqualValues(fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId), message.Event.Join[0].RoomSessionId) - assert.Equal(features, message.Event.Join[0].Features) - } - } -} - -func TestRoom_RoomSessionData(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - require := require.New(t) - assert := assert.New(t) - hub, _, router, server := CreateHubForTest(t) - - config, err := getTestConfig(server) - require.NoError(err) - b, err := NewBackendServer(ctx, config, hub, "no-version") - require.NoError(err) - require.NoError(b.Start(router)) - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, authAnonymousUserId) - - // Join room by id. - roomId := "test-room-with-sessiondata" - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // We will receive a "joined" event with the userid from the room session data. - expected := "userid-from-sessiondata" - if message, ok := client.RunUntilMessage(ctx); ok { - if client.checkMessageJoinedSession(message, hello.Hello.SessionId, expected) { - assert.EqualValues(fmt.Sprintf("%s-%s", roomId, hello.Hello.SessionId), message.Event.Join[0].RoomSessionId) - } - } - - session := hub.GetSessionByPublicId(hello.Hello.SessionId) - require.NotNil(session, "Could not find session %s", hello.Hello.SessionId) - assert.Equal(expected, session.UserId()) - - room := hub.getRoom(roomId) - assert.NotNil(room, "Room not found") - - entries, wg := room.publishActiveSessions() - assert.Equal(1, entries) - wg.Wait() -} - -func TestRoom_InCall(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - require := require.New(t) - assert := assert.New(t) - hub, _, router, server := CreateHubForTest(t) - - config, err := getTestConfig(server) - require.NoError(err) - b, err := NewBackendServer(ctx, config, hub, "no-version") - require.NoError(err) - require.NoError(b.Start(router)) - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client1.RunUntilJoined(ctx, hello1.Hello) - - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) - - client1.RunUntilJoined(ctx, hello2.Hello) - - msg1 := &talk.BackendServerRoomRequest{ - Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ - InCall: json.RawMessage(strconv.FormatInt(FlagInCall, 10)), - Changed: []api.StringMap{ - { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), - "inCall": json.RawMessage(strconv.FormatInt(FlagInCall, 10)), - }, - }, - Users: []api.StringMap{ - { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), - "inCall": json.RawMessage(strconv.FormatInt(FlagInCall, 10)), - }, - { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello2.Hello.SessionId), - "inCall": json.RawMessage(strconv.FormatInt(0, 10)), - }, - }, - }, - } - - data1, err := json.Marshal(msg1) - require.NoError(err) - res1, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data1) - require.NoError(err) - defer res1.Body.Close() - body1, err := io.ReadAll(res1.Body) - assert.NoError(err) - assert.Equal(http.StatusOK, res1.StatusCode, "Expected successful request, got %s", string(body1)) - - if msg, ok := client1.RunUntilMessage(ctx); ok { - if message, ok := checkMessageParticipantsInCall(t, msg); ok { - assert.Equal(roomId, message.RoomId) - if assert.Len(message.Users, 2) { - assert.EqualValues(hello1.Hello.SessionId, message.Users[0]["sessionId"]) - assert.EqualValues(FlagInCall, message.Users[0]["inCall"]) - assert.EqualValues(hello2.Hello.SessionId, message.Users[1]["sessionId"]) - assert.EqualValues(0, message.Users[1]["inCall"]) - } - } - } - - if msg, ok := client2.RunUntilMessage(ctx); ok { - if message, ok := checkMessageParticipantsInCall(t, msg); ok { - assert.Equal(roomId, message.RoomId) - if assert.Len(message.Users, 2) { - assert.EqualValues(hello1.Hello.SessionId, message.Users[0]["sessionId"]) - assert.EqualValues(FlagInCall, message.Users[0]["inCall"]) - assert.EqualValues(hello2.Hello.SessionId, message.Users[1]["sessionId"]) - assert.EqualValues(0, message.Users[1]["inCall"]) - } - } - } - - msg2 := &talk.BackendServerRoomRequest{ - Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ - InCall: json.RawMessage(strconv.FormatInt(0, 10)), - Changed: []api.StringMap{ - { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), - "inCall": json.RawMessage(strconv.FormatInt(0, 10)), - }, - }, - Users: []api.StringMap{ - { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello1.Hello.SessionId), - "inCall": json.RawMessage(strconv.FormatInt(0, 10)), - }, - { - "sessionId": fmt.Sprintf("%s-%s", roomId, hello2.Hello.SessionId), - "inCall": json.RawMessage(strconv.FormatInt(0, 10)), - }, - }, - }, - } - - data2, err := json.Marshal(msg2) - require.NoError(err) - res2, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data2) - require.NoError(err) - defer res2.Body.Close() - body2, err := io.ReadAll(res2.Body) - assert.NoError(err) - assert.Equal(http.StatusOK, res2.StatusCode, "Expected successful request, got %s", string(body2)) - - if msg, ok := client1.RunUntilMessage(ctx); ok { - if message, ok := checkMessageParticipantsInCall(t, msg); ok { - assert.Equal(roomId, message.RoomId) - if assert.Len(message.Users, 2) { - assert.EqualValues(hello1.Hello.SessionId, message.Users[0]["sessionId"]) - assert.EqualValues(0, message.Users[0]["inCall"]) - assert.EqualValues(hello2.Hello.SessionId, message.Users[1]["sessionId"]) - assert.EqualValues(0, message.Users[1]["inCall"]) - } - } - } - - if msg, ok := client2.RunUntilMessage(ctx); ok { - if message, ok := checkMessageParticipantsInCall(t, msg); ok { - assert.Equal(roomId, message.RoomId) - if assert.Len(message.Users, 2) { - assert.EqualValues(hello1.Hello.SessionId, message.Users[0]["sessionId"]) - assert.EqualValues(0, message.Users[0]["inCall"]) - assert.EqualValues(hello2.Hello.SessionId, message.Users[1]["sessionId"]) - assert.EqualValues(0, message.Users[1]["inCall"]) - } - } - } -} - -func TestRoom_InCallAll(t *testing.T) { - t.Parallel() - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - require := require.New(t) - assert := assert.New(t) - hub, _, router, server := CreateHubForTest(t) - - config, err := getTestConfig(server) - require.NoError(err) - b, err := NewBackendServer(ctx, config, hub, "no-version") - require.NoError(err) - require.NoError(b.Start(router)) - - ctx, cancel := context.WithTimeout(ctx, testTimeout) - defer cancel() - - client1, hello1 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"1") - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - - // Join room by id. - roomId := "test-room" - roomMsg := MustSucceed2(t, client1.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client1.RunUntilJoined(ctx, hello1.Hello) - - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) - - client1.RunUntilJoined(ctx, hello2.Hello) - - // Simulate backend request from Nextcloud to update the "inCall" flag of all participants. - msg1 := &talk.BackendServerRoomRequest{ - Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ - All: true, - InCall: json.RawMessage(strconv.FormatInt(FlagInCall, 10)), - }, - } - - data1, err := json.Marshal(msg1) - require.NoError(err) - res1, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data1) - require.NoError(err) - defer res1.Body.Close() - body1, err := io.ReadAll(res1.Body) - assert.NoError(err) - assert.Equal(http.StatusOK, res1.StatusCode, "Expected successful request, got %s", string(body1)) - - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageInCallAll(t, msg, roomId, FlagInCall) - } - - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageInCallAll(t, msg, roomId, FlagInCall) - } - - // Simulate backend request from Nextcloud to update the "inCall" flag of all participants. - msg2 := &talk.BackendServerRoomRequest{ - Type: "incall", - InCall: &talk.BackendRoomInCallRequest{ - All: true, - InCall: json.RawMessage(strconv.FormatInt(0, 10)), - }, - } - - data2, err := json.Marshal(msg2) - require.NoError(err) - res2, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data2) - require.NoError(err) - defer res2.Body.Close() - body2, err := io.ReadAll(res2.Body) - assert.NoError(err) - assert.Equal(http.StatusOK, res2.StatusCode, "Expected successful request, got %s", string(body2)) - - if msg, ok := client1.RunUntilMessage(ctx); ok { - checkMessageInCallAll(t, msg, roomId, 0) - } - - if msg, ok := client2.RunUntilMessage(ctx); ok { - checkMessageInCallAll(t, msg, roomId, 0) - } -} diff --git a/server/testclient_test.go b/server/testclient_test.go deleted file mode 100644 index 62d55cd..0000000 --- a/server/testclient_test.go +++ /dev/null @@ -1,1157 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "net" - "net/http/httptest" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/golang-jwt/jwt/v5" - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/session" -) - -var ( - testBackendSecret = []byte("secret") - testInternalSecret = []byte("internal-secret") - - ErrNoMessageReceived = errors.New("no message was received by the server") - - testClientDialer = websocket.Dialer{ - WriteBufferPool: &sync.Pool{}, - } -) - -type TestBackendClientAuthParams struct { - UserId string `json:"userid"` -} - -func getWebsocketUrl(url string) string { - if url, found := strings.CutPrefix(url, "http://"); found { - return "ws://" + url + "/spreed" - } else if url, found := strings.CutPrefix(url, "https://"); found { - return "wss://" + url + "/spreed" - } else { - panic("Unsupported URL: " + url) - } -} - -func getPubliceSessionIdData(h *Hub, publicId api.PublicSessionId) *session.SessionIdData { - decodedPublic := h.decodePublicSessionId(publicId) - if decodedPublic == nil { - panic("invalid public session id") - } - return decodedPublic -} - -func checkMessageType(t *testing.T, message *api.ServerMessage, expectedType string) bool { - assert := assert.New(t) - if !assert.NotNil(message, "no message received") { - return false - } - - failed := !assert.Equal(expectedType, message.Type, "invalid message type in %+v", message) - - switch message.Type { - case "hello": - if !assert.NotNil(message.Hello, "hello missing in %+v", message) { - failed = true - } - case "message": - if assert.NotNil(message.Message, "message missing in %+v", message) { - if !assert.NotEmpty(message.Message.Data, "message %+v has no data", message) { - failed = true - } - } else { - failed = true - } - case "room": - if !assert.NotNil(message.Room, "room missing in %+v", message) { - failed = true - } - case "event": - if !assert.NotNil(message.Event, "event missing in %+v", message) { - failed = true - } - case "transient": - if !assert.NotNil(message.TransientData, "transient data missing in %+v", message) { - failed = true - } - } - return !failed -} - -func checkMessageSender(t *testing.T, hub *Hub, sender *api.MessageServerMessageSender, senderType string, hello *api.HelloServerMessage) bool { - assert := assert.New(t) - return assert.Equal(senderType, sender.Type, "invalid sender type in %+v", sender) && - assert.Equal(hello.SessionId, sender.SessionId, "invalid session id, expectd %+v, got %+v in %+v", - getPubliceSessionIdData(hub, hello.SessionId), - getPubliceSessionIdData(hub, sender.SessionId), - sender, - ) && - assert.Equal(hello.UserId, sender.UserId, "invalid userid in %+v", sender) -} - -func checkReceiveClientMessageWithSenderAndRecipient(ctx context.Context, t *testing.T, client *TestClient, senderType string, hello *api.HelloServerMessage, payload any, sender **api.MessageServerMessageSender, recipient **api.MessageClientMessageRecipient) bool { - assert := assert.New(t) - message, ok := client.RunUntilMessage(ctx) - if !ok || - !checkMessageType(t, message, "message") || - !checkMessageSender(t, client.hub, message.Message.Sender, senderType, hello) || - !assert.NoError(json.Unmarshal(message.Message.Data, payload)) { - return false - } - - if sender != nil { - *sender = message.Message.Sender - } - if recipient != nil { - *recipient = message.Message.Recipient - } - return true -} - -func checkReceiveClientMessageWithSender(ctx context.Context, t *testing.T, client *TestClient, senderType string, hello *api.HelloServerMessage, payload any, sender **api.MessageServerMessageSender) bool { - return checkReceiveClientMessageWithSenderAndRecipient(ctx, t, client, senderType, hello, payload, sender, nil) -} - -func checkReceiveClientMessage(ctx context.Context, t *testing.T, client *TestClient, senderType string, hello *api.HelloServerMessage, payload any) bool { - return checkReceiveClientMessageWithSenderAndRecipient(ctx, t, client, senderType, hello, payload, nil, nil) -} - -func checkReceiveClientControlWithSenderAndRecipient(ctx context.Context, t *testing.T, client *TestClient, senderType string, hello *api.HelloServerMessage, payload any, sender **api.MessageServerMessageSender, recipient **api.MessageClientMessageRecipient) bool { - assert := assert.New(t) - message, ok := client.RunUntilMessage(ctx) - if !ok || - !checkMessageType(t, message, "control") || - !checkMessageSender(t, client.hub, message.Control.Sender, senderType, hello) || - !assert.NoError(json.Unmarshal(message.Control.Data, payload)) { - return false - } - - if sender != nil { - *sender = message.Control.Sender - } - if recipient != nil { - *recipient = message.Control.Recipient - } - return true -} - -func checkReceiveClientControlWithSender(ctx context.Context, t *testing.T, client *TestClient, senderType string, hello *api.HelloServerMessage, payload any, sender **api.MessageServerMessageSender) bool { // nolint - return checkReceiveClientControlWithSenderAndRecipient(ctx, t, client, senderType, hello, payload, sender, nil) -} - -func checkReceiveClientControl(ctx context.Context, t *testing.T, client *TestClient, senderType string, hello *api.HelloServerMessage, payload any) bool { - return checkReceiveClientControlWithSenderAndRecipient(ctx, t, client, senderType, hello, payload, nil, nil) -} - -func checkReceiveClientEvent(ctx context.Context, t *testing.T, client *TestClient, eventType string, msg **api.EventServerMessage) bool { - assert := assert.New(t) - message, ok := client.RunUntilMessage(ctx) - if !ok || - !checkMessageType(t, message, "event") || - !assert.Equal(eventType, message.Event.Type, "invalid event type in %+v", message) { - return false - } - - if msg != nil { - *msg = message.Event - } - return true -} - -type TestClient struct { - t *testing.T - assert *assert.Assertions - require *require.Assertions - hub *Hub - server *httptest.Server - - mu sync.Mutex - // +checklocks:mu - conn *websocket.Conn - localAddr net.Addr - - messageChan chan []byte - readErrorChan chan error - - publicId api.PublicSessionId -} - -func NewTestClientContext(ctx context.Context, t *testing.T, server *httptest.Server, hub *Hub) *TestClient { - // Reference "hub" to prevent compiler error. - conn, _, err := testClientDialer.DialContext(ctx, getWebsocketUrl(server.URL), nil) - require.NoError(t, err) - - messageChan := make(chan []byte) - readErrorChan := make(chan error, 1) - - closing := make(chan struct{}) - closed := make(chan struct{}) - go func() { - defer close(closed) - for { - messageType, data, err := conn.ReadMessage() - if err != nil { - readErrorChan <- err - return - } else if !assert.Equal(t, websocket.TextMessage, messageType) { - return - } - - select { - case messageChan <- data: - case <-closing: - return - } - } - }() - t.Cleanup(func() { - close(closing) - <-closed - }) - - return &TestClient{ - t: t, - assert: assert.New(t), - require: require.New(t), - hub: hub, - server: server, - - conn: conn, - localAddr: conn.LocalAddr(), - - messageChan: messageChan, - readErrorChan: readErrorChan, - } -} - -func NewTestClient(t *testing.T, server *httptest.Server, hub *Hub) *TestClient { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - client := NewTestClientContext(ctx, t, server, hub) - if msg, ok := client.RunUntilMessage(ctx); ok { - assert.Equal(t, "welcome", msg.Type, "invalid initial message type in %+v", msg) - } - return client -} - -func NewTestClientWithHello(ctx context.Context, t *testing.T, server *httptest.Server, hub *Hub, userId string) (*TestClient, *api.ServerMessage) { - client := NewTestClient(t, server, hub) - t.Cleanup(func() { - client.CloseWithBye() - }) - - require.NoError(t, client.SendHello(userId)) - hello := MustSucceed1(t, client.RunUntilHello, ctx) - return client, hello -} - -func (c *TestClient) CloseWithBye() { - c.SendBye() // nolint - c.Close() -} - -func (c *TestClient) Close() { - c.mu.Lock() - defer c.mu.Unlock() - if err := c.conn.WriteMessage(websocket.CloseMessage, []byte{}); err == websocket.ErrCloseSent { - // Already closed - return - } - - // Wait a bit for close message to be processed. - time.Sleep(100 * time.Millisecond) - c.conn.Close() - - // Drain any entries in the channels to terminate the read goroutine. -loop: - for { - select { - case <-c.readErrorChan: - case <-c.messageChan: - default: - break loop - } - } -} - -func (c *TestClient) WaitForClientRemoved(ctx context.Context) error { - c.hub.mu.Lock() - defer c.hub.mu.Unlock() - for { - found := false - for _, client := range c.hub.clients { - if cc, ok := client.(*HubClient); ok { - conn := cc.GetConn() - if conn != nil && conn.RemoteAddr().String() == c.localAddr.String() { - found = true - break - } - } - } - if !found { - break - } - - c.hub.mu.Unlock() - select { - case <-ctx.Done(): - c.hub.mu.Lock() - return ctx.Err() - default: - time.Sleep(time.Millisecond) - } - c.hub.mu.Lock() - } - return nil -} - -func (c *TestClient) WaitForSessionRemoved(ctx context.Context, sessionId api.PublicSessionId) error { - data := c.hub.decodePublicSessionId(sessionId) - if data == nil { - return errors.New("Invalid session id passed") - } - - c.hub.mu.Lock() - defer c.hub.mu.Unlock() - for { - _, found := c.hub.sessions[data.Sid] - if !found { - break - } - - c.hub.mu.Unlock() - select { - case <-ctx.Done(): - c.hub.mu.Lock() - return ctx.Err() - default: - time.Sleep(time.Millisecond) - } - c.hub.mu.Lock() - } - return nil -} - -func (c *TestClient) WriteJSON(data any) error { - if !strings.Contains(c.t.Name(), "HelloUnsupportedVersion") { - if msg, ok := data.(*api.ClientMessage); ok { - if err := msg.CheckValid(); err != nil { - return err - } - } - } - - c.mu.Lock() - defer c.mu.Unlock() - return c.conn.WriteJSON(data) -} - -func (c *TestClient) EnsuerWriteJSON(data any) { - require.NoError(c.t, c.WriteJSON(data), "Could not write JSON %+v", data) -} - -func (c *TestClient) SendHello(userid string) error { - return c.SendHelloV1(userid) -} - -func (c *TestClient) SendHelloV1(userid string) error { - params := TestBackendClientAuthParams{ - UserId: userid, - } - return c.SendHelloParams(c.server.URL, api.HelloVersionV1, "", nil, params) -} - -func (c *TestClient) SendHelloV2(userid string) error { - return c.SendHelloV2WithFeatures(userid, nil) -} - -func (c *TestClient) SendHelloV2WithFeatures(userid string, features []string) error { - now := time.Now() - return c.SendHelloV2WithTimesAndFeatures(userid, now, now.Add(time.Minute), features) -} - -func (c *TestClient) CreateHelloV2TokenWithUserdata(userid string, issuedAt time.Time, expiresAt time.Time, userdata api.StringMap) (string, error) { - data, err := json.Marshal(userdata) - if err != nil { - return "", err - } - - claims := &api.HelloV2TokenClaims{ - RegisteredClaims: jwt.RegisteredClaims{ - Issuer: c.server.URL, - Subject: userid, - }, - UserData: data, - } - if !issuedAt.IsZero() { - claims.IssuedAt = jwt.NewNumericDate(issuedAt) - } - if !expiresAt.IsZero() { - claims.ExpiresAt = jwt.NewNumericDate(expiresAt) - } - - var token *jwt.Token - if strings.Contains(c.t.Name(), "ECDSA") { - token = jwt.NewWithClaims(jwt.SigningMethodES256, claims) - } else if strings.Contains(c.t.Name(), "Ed25519") { - token = jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims) - } else { - token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - } - private := getPrivateAuthToken(c.t) - return token.SignedString(private) -} - -func (c *TestClient) CreateHelloV2Token(userid string, issuedAt time.Time, expiresAt time.Time) (string, error) { - userdata := api.StringMap{ - "displayname": "Displayname " + userid, - } - - return c.CreateHelloV2TokenWithUserdata(userid, issuedAt, expiresAt, userdata) -} - -func (c *TestClient) SendHelloV2WithTimes(userid string, issuedAt time.Time, expiresAt time.Time) error { - return c.SendHelloV2WithTimesAndFeatures(userid, issuedAt, expiresAt, nil) -} - -func (c *TestClient) SendHelloV2WithTimesAndFeatures(userid string, issuedAt time.Time, expiresAt time.Time, features []string) error { - tokenString, err := c.CreateHelloV2Token(userid, issuedAt, expiresAt) - require.NoError(c.t, err) - - params := api.HelloV2AuthParams{ - Token: tokenString, - } - return c.SendHelloParams(c.server.URL, api.HelloVersionV2, "", features, params) -} - -func (c *TestClient) SendHelloResume(resumeId api.PrivateSessionId) error { - hello := &api.ClientMessage{ - Id: "1234", - Type: "hello", - Hello: &api.HelloClientMessage{ - Version: api.HelloVersionV1, - ResumeId: resumeId, - }, - } - return c.WriteJSON(hello) -} - -func (c *TestClient) SendHelloClient(userid string) error { - return c.SendHelloClientWithFeatures(userid, nil) -} - -func (c *TestClient) SendHelloClientWithFeatures(userid string, features []string) error { - params := TestBackendClientAuthParams{ - UserId: userid, - } - return c.SendHelloParams(c.server.URL, api.HelloVersionV1, "client", features, params) -} - -func (c *TestClient) SendHelloInternal() error { - return c.SendHelloInternalWithFeatures(nil) -} - -func (c *TestClient) SendHelloInternalWithFeatures(features []string) error { - random := internal.RandomString(48) - mac := hmac.New(sha256.New, testInternalSecret) - mac.Write([]byte(random)) // nolint - token := hex.EncodeToString(mac.Sum(nil)) - backend := c.server.URL - - params := api.ClientTypeInternalAuthParams{ - Random: random, - Token: token, - Backend: backend, - } - return c.SendHelloParams("", api.HelloVersionV1, "internal", features, params) -} - -func (c *TestClient) SendHelloParams(url string, version string, clientType api.ClientType, features []string, params any) error { - data, err := json.Marshal(params) - require.NoError(c.t, err) - - hello := &api.ClientMessage{ - Id: "1234", - Type: "hello", - Hello: &api.HelloClientMessage{ - Version: version, - Features: features, - Auth: &api.HelloClientMessageAuth{ - Type: clientType, - Url: url, - Params: data, - }, - }, - } - return c.WriteJSON(hello) -} - -func (c *TestClient) SendBye() error { - hello := &api.ClientMessage{ - Id: "9876", - Type: "bye", - Bye: &api.ByeClientMessage{}, - } - return c.WriteJSON(hello) -} - -func (c *TestClient) SendMessage(recipient api.MessageClientMessageRecipient, data any) error { - payload, err := json.Marshal(data) - require.NoError(c.t, err) - - message := &api.ClientMessage{ - Id: "abcd", - Type: "message", - Message: &api.MessageClientMessage{ - Recipient: recipient, - Data: payload, - }, - } - return c.WriteJSON(message) -} - -func (c *TestClient) SendControl(recipient api.MessageClientMessageRecipient, data any) error { - payload, err := json.Marshal(data) - require.NoError(c.t, err) - - message := &api.ClientMessage{ - Id: "abcd", - Type: "control", - Control: &api.ControlClientMessage{ - MessageClientMessage: api.MessageClientMessage{ - Recipient: recipient, - Data: payload, - }, - }, - } - return c.WriteJSON(message) -} - -func (c *TestClient) SendInternalAddSession(msg *api.AddSessionInternalClientMessage) error { - message := &api.ClientMessage{ - Id: "abcd", - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "addsession", - AddSession: msg, - }, - } - return c.WriteJSON(message) -} - -func (c *TestClient) SendInternalUpdateSession(msg *api.UpdateSessionInternalClientMessage) error { - message := &api.ClientMessage{ - Id: "abcd", - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "updatesession", - UpdateSession: msg, - }, - } - return c.WriteJSON(message) -} - -func (c *TestClient) SendInternalRemoveSession(msg *api.RemoveSessionInternalClientMessage) error { - message := &api.ClientMessage{ - Id: "abcd", - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "removesession", - RemoveSession: msg, - }, - } - return c.WriteJSON(message) -} - -func (c *TestClient) SendInternalDialout(msg *api.DialoutInternalClientMessage) error { - message := &api.ClientMessage{ - Id: "abcd", - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "dialout", - Dialout: msg, - }, - } - return c.WriteJSON(message) -} - -func (c *TestClient) SetTransientData(key string, value any, ttl time.Duration) error { - payload, err := json.Marshal(value) - require.NoError(c.t, err) - - message := &api.ClientMessage{ - Id: "efgh", - Type: "transient", - TransientData: &api.TransientDataClientMessage{ - Type: "set", - Key: key, - Value: payload, - TTL: ttl, - }, - } - return c.WriteJSON(message) -} - -func (c *TestClient) RemoveTransientData(key string) error { - message := &api.ClientMessage{ - Id: "ijkl", - Type: "transient", - TransientData: &api.TransientDataClientMessage{ - Type: "remove", - Key: key, - }, - } - return c.WriteJSON(message) -} - -func (c *TestClient) GetPendingMessages(ctx context.Context) ([]*api.ServerMessage, error) { - var result []*api.ServerMessage - select { - case err := <-c.readErrorChan: - return nil, err - case msg := <-c.messageChan: - var m api.ServerMessage - if err := json.Unmarshal(msg, &m); err != nil { - return nil, err - } - result = append(result, &m) - n := len(c.messageChan) - for range n { - var m api.ServerMessage - msg = <-c.messageChan - if err := json.Unmarshal(msg, &m); err != nil { - return nil, err - } - result = append(result, &m) - } - case <-ctx.Done(): - return nil, ctx.Err() - } - return result, nil -} - -func (c *TestClient) RunUntilClosed(ctx context.Context) bool { - select { - case err := <-c.readErrorChan: - if c.assert.Error(err) && websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { - return true - } - - c.assert.NoError(err, "Received unexpected error") - case msg := <-c.messageChan: - var m api.ServerMessage - if err := json.Unmarshal(msg, &m); c.assert.NoError(err, "error decoding received message") { - c.assert.Fail("Server should have closed the connection", "received %+v", m) - } - case <-ctx.Done(): - c.assert.NoError(ctx.Err(), "error while waiting for closed connection") - } - return false -} - -func (c *TestClient) RunUntilErrorIs(ctx context.Context, targets ...error) bool { - var err error - select { - case err = <-c.readErrorChan: - case msg := <-c.messageChan: - var m api.ServerMessage - if err := json.Unmarshal(msg, &m); c.assert.NoError(err, "error decoding received message") { - c.assert.Fail("received message", "expected one of errors %+v, got message %+v", targets, m) - } - return false - case <-ctx.Done(): - err = ctx.Err() - } - - if c.assert.Error(err, "expected one of errors %+v", targets) { - for _, t := range targets { - if errors.Is(err, t) { - return true - } - } - - c.assert.Fail("invalid error", "expected one of errors %+v, got %s", targets, err) - } - - return false -} - -func (c *TestClient) RunUntilMessage(ctx context.Context) (*api.ServerMessage, bool) { - select { - case err := <-c.readErrorChan: - c.assert.NoError(err, "error reading while waiting for message") - return nil, false - case msg := <-c.messageChan: - var m api.ServerMessage - if err := json.Unmarshal(msg, &m); c.assert.NoError(err, "error decoding received message") { - return &m, true - } - case <-ctx.Done(): - c.assert.NoError(ctx.Err(), "error while waiting for message") - } - return nil, false -} - -func (c *TestClient) RunUntilMessageOrClosed(ctx context.Context) (*api.ServerMessage, bool) { - select { - case err := <-c.readErrorChan: - if c.assert.Error(err) && websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { - return nil, true - } - - c.assert.NoError(err, "Received unexpected error") - return nil, false - case msg := <-c.messageChan: - var m api.ServerMessage - if err := json.Unmarshal(msg, &m); c.assert.NoError(err, "error decoding received message") { - return &m, true - } - case <-ctx.Done(): - c.assert.NoError(ctx.Err(), "error while waiting for message") - } - return nil, false -} - -func (c *TestClient) RunUntilError(ctx context.Context, code string) (*api.Error, bool) { - message, ok := c.RunUntilMessage(ctx) - if !ok || - !checkMessageType(c.t, message, "error") || - !c.assert.Equal(code, message.Error.Code, "invalid error code in %+v", message) { - return nil, false - } - - return message.Error, true -} - -func (c *TestClient) RunUntilHello(ctx context.Context) (*api.ServerMessage, bool) { - message, ok := c.RunUntilMessage(ctx) - if !ok || - !checkMessageType(c.t, message, "hello") { - return nil, false - } - - c.publicId = message.Hello.SessionId - return message, true -} - -func (c *TestClient) JoinRoom(ctx context.Context, roomId string) (*api.ServerMessage, bool) { - return c.JoinRoomWithRoomSession(ctx, roomId, api.RoomSessionId(fmt.Sprintf("%s-%s", roomId, c.publicId))) -} - -func (c *TestClient) JoinRoomWithRoomSession(ctx context.Context, roomId string, roomSessionId api.RoomSessionId) (message *api.ServerMessage, ok bool) { - msg := &api.ClientMessage{ - Id: "ABCD", - Type: "room", - Room: &api.RoomClientMessage{ - RoomId: roomId, - SessionId: roomSessionId, - }, - } - if err := c.WriteJSON(msg); !c.assert.NoError(err) { - return nil, false - } - - if message, ok = c.RunUntilMessage(ctx); !ok || - !checkMessageType(c.t, message, "room") || - !c.assert.Equal(msg.Id, message.Id, "invalid message id in %+v", message) { - return nil, false - } - - return message, true -} - -func checkMessageRoomId(t *testing.T, message *api.ServerMessage, roomId string) bool { - return checkMessageType(t, message, "room") && - assert.Equal(t, roomId, message.Room.RoomId, "invalid room id in %+v", message) -} - -func (c *TestClient) RunUntilRoom(ctx context.Context, roomId string) bool { - message, ok := c.RunUntilMessage(ctx) - return ok && checkMessageRoomId(c.t, message, roomId) -} - -func (c *TestClient) checkMessageJoined(message *api.ServerMessage, hello *api.HelloServerMessage) bool { - return c.checkMessageJoinedSession(message, hello.SessionId, hello.UserId) -} - -func (c *TestClient) checkSingleMessageJoined(message *api.ServerMessage) bool { - return checkMessageType(c.t, message, "event") && - c.assert.Equal("room", message.Event.Target, "invalid event target in %+v", message) && - c.assert.Equal("join", message.Event.Type, "invalid event type in %+v", message) && - c.assert.Len(message.Event.Join, 1, "invalid number of join event entries in %+v", message) -} - -func (c *TestClient) checkMessageJoinedSession(message *api.ServerMessage, sessionId api.PublicSessionId, userId string) bool { - if !c.checkSingleMessageJoined(message) { - return false - } - - failed := false - evt := message.Event.Join[0] - if sessionId != "" { - if !c.assert.Equal(sessionId, evt.SessionId, "invalid join session id: expected %+v, got %+v in %+v", - getPubliceSessionIdData(c.hub, sessionId), - getPubliceSessionIdData(c.hub, evt.SessionId), - message, - ) { - failed = true - } - } - if !c.assert.Equal(userId, evt.UserId, "invalid user id in %+v", evt) { - failed = true - } - return !failed -} - -func (c *TestClient) RunUntilJoinedAndReturn(ctx context.Context, hello ...*api.HelloServerMessage) ([]api.EventServerMessageSessionEntry, []*api.ServerMessage, bool) { - received := make([]api.EventServerMessageSessionEntry, len(hello)) - var ignored []*api.ServerMessage - hellos := make(map[*api.HelloServerMessage]int, len(hello)) - for idx, h := range hello { - hellos[h] = idx - } - for len(hellos) > 0 { - message, ok := c.RunUntilMessage(ctx) - if !ok { - return nil, nil, false - } - - if message.Type != "event" || message.Event == nil { - ignored = append(ignored, message) - continue - } else if message.Event.Target != "room" || message.Event.Type != "join" { - ignored = append(ignored, message) - continue - } - - if !checkMessageType(c.t, message, "event") { - continue - } - - for len(message.Event.Join) > 0 { - found := false - loop: - for h, idx := range hellos { - for idx2, evt := range message.Event.Join { - if evt.SessionId == h.SessionId && evt.UserId == h.UserId { - received[idx] = evt - delete(hellos, h) - message.Event.Join = append(message.Event.Join[:idx2], message.Event.Join[idx2+1:]...) - found = true - break loop - } - } - } - if !c.assert.True(found, "expected one of the passed hello sessions, got %+v", message.Event.Join) { - break - } - } - } - return received, ignored, true -} - -func (c *TestClient) RunUntilJoined(ctx context.Context, hello ...*api.HelloServerMessage) bool { - _, unexpected, ok := c.RunUntilJoinedAndReturn(ctx, hello...) - return ok && c.assert.Empty(unexpected, "Received unexpected messages: %+v", unexpected) -} - -func (c *TestClient) checkMessageRoomLeave(message *api.ServerMessage, hello *api.HelloServerMessage) bool { - return c.checkMessageRoomLeaveSession(message, hello.SessionId) -} - -func (c *TestClient) checkMessageRoomLeaveSession(message *api.ServerMessage, sessionId api.PublicSessionId) bool { - return checkMessageType(c.t, message, "event") && - c.assert.Equal("room", message.Event.Target, "invalid target in %+v", message) && - c.assert.Equal("leave", message.Event.Type, "invalid event type in %+v", message) && - c.assert.Len(message.Event.Leave, 1, "invalid number of leave event entries: %+v", message.Event) && - c.assert.Equal(sessionId, message.Event.Leave[0], "invalid leave session: expected %+v, got %+v in %+v", - getPubliceSessionIdData(c.hub, sessionId), - getPubliceSessionIdData(c.hub, message.Event.Leave[0]), - message, - ) -} - -func (c *TestClient) RunUntilLeft(ctx context.Context, hello *api.HelloServerMessage) bool { - message, ok := c.RunUntilMessage(ctx) - return ok && c.checkMessageRoomLeave(message, hello) -} - -func checkMessageRoomlistUpdate(t *testing.T, message *api.ServerMessage) (*api.RoomEventServerMessage, bool) { - assert := assert.New(t) - if !checkMessageType(t, message, "event") || - !assert.Equal("roomlist", message.Event.Target, "invalid event target in %+v", message) || - !assert.Equal("update", message.Event.Type, "invalid event type in %+v", message) || - !assert.NotNil(message.Event.Update, "update missing in %+v", message) { - return nil, false - } - - return message.Event.Update, true -} - -func (c *TestClient) RunUntilRoomlistUpdate(ctx context.Context) (*api.RoomEventServerMessage, bool) { - message, ok := c.RunUntilMessage(ctx) - if !ok { - return nil, false - } - - return checkMessageRoomlistUpdate(c.t, message) -} - -func checkMessageRoomlistDisinvite(t *testing.T, message *api.ServerMessage) (*api.RoomDisinviteEventServerMessage, bool) { - assert := assert.New(t) - if !checkMessageType(t, message, "event") || - !assert.Equal("roomlist", message.Event.Target, "invalid event target in %+v", message) || - !assert.Equal("disinvite", message.Event.Type, "invalid event type in %+v", message) || - !assert.NotNil(message.Event.Disinvite, "disinvite missing in %+v", message) { - return nil, false - } - - return message.Event.Disinvite, true -} - -func (c *TestClient) RunUntilRoomlistDisinvite(ctx context.Context) (*api.RoomDisinviteEventServerMessage, bool) { - message, ok := c.RunUntilMessage(ctx) - if !ok { - return nil, false - } - - return checkMessageRoomlistDisinvite(c.t, message) -} - -func checkMessageParticipantsInCall(t *testing.T, message *api.ServerMessage) (*api.RoomEventServerMessage, bool) { - assert := assert.New(t) - if !checkMessageType(t, message, "event") || - !assert.Equal("participants", message.Event.Target, "invalid event target in %+v", message) || - !assert.Equal("update", message.Event.Type, "invalid event type in %+v", message) || - !assert.NotNil(message.Event.Update, "update missing in %+v", message) { - return nil, false - } - - return message.Event.Update, true -} - -func checkMessageParticipantFlags(t *testing.T, message *api.ServerMessage) (*api.RoomFlagsServerMessage, bool) { - assert := assert.New(t) - if !checkMessageType(t, message, "event") || - !assert.Equal("participants", message.Event.Target, "invalid event target in %+v", message) || - !assert.Equal("flags", message.Event.Type, "invalid event type in %+v", message) || - !assert.NotNil(message.Event.Flags, "flags missing in %+v", message) { - return nil, false - } - - return message.Event.Flags, true -} - -func checkMessageRoomMessage(t *testing.T, message *api.ServerMessage) (*api.RoomEventMessage, bool) { - assert := assert.New(t) - if !checkMessageType(t, message, "event") || - !assert.Equal("room", message.Event.Target, "invalid event target in %+v", message) || - !assert.Equal("message", message.Event.Type, "invalid event type in %+v", message) || - !assert.NotNil(message.Event.Message, "message missing in %+v", message) { - return nil, false - } - - return message.Event.Message, true -} - -func (c *TestClient) RunUntilRoomMessage(ctx context.Context) (*api.RoomEventMessage, bool) { - message, ok := c.RunUntilMessage(ctx) - if !ok { - return nil, false - } - - return checkMessageRoomMessage(c.t, message) -} - -func checkMessageError(t *testing.T, message *api.ServerMessage, msgid string) bool { - return checkMessageType(t, message, "error") && - assert.Equal(t, msgid, message.Error.Code, "invalid error code in %+v", message) -} - -func (c *TestClient) RunUntilOffer(ctx context.Context, offer string) bool { - message, ok := c.RunUntilMessage(ctx) - if !ok || !checkMessageType(c.t, message, "message") { - return false - } - - var data api.StringMap - if err := json.Unmarshal(message.Message.Data, &data); !c.assert.NoError(err) { - return false - } - - if dt, ok := api.GetStringMapEntry[string](data, "type"); !c.assert.True(ok, "no/invalid type in %+v", data) || - !c.assert.Equal("offer", dt, "invalid data type in %+v", data) { - return false - } - - if payload, ok := api.ConvertStringMap(data["payload"]); !c.assert.True(ok, "not a string map, got %+v", data["payload"]) { - return false - } else { - if pt, ok := api.GetStringMapEntry[string](payload, "type"); !c.assert.True(ok, "no/invalid type in payload %+v", payload) || - !c.assert.Equal("offer", pt, "invalid payload type in %+v", payload) { - return false - } - if sdp, ok := api.GetStringMapEntry[string](payload, "sdp"); !c.assert.True(ok, "no/invalid sdp in payload %+v", payload) || - !c.assert.Equal(offer, sdp, "invalid payload offer") { - return false - } - } - - return true -} - -func (c *TestClient) RunUntilAnswer(ctx context.Context, answer string) bool { - return c.RunUntilAnswerFromSender(ctx, answer, nil) -} - -func (c *TestClient) RunUntilAnswerFromSender(ctx context.Context, answer string, sender *api.MessageServerMessageSender) bool { - message, ok := c.RunUntilMessage(ctx) - if !ok || !checkMessageType(c.t, message, "message") { - return false - } - - if sender != nil { - if !checkMessageSender(c.t, c.hub, message.Message.Sender, sender.Type, &api.HelloServerMessage{ - SessionId: sender.SessionId, - UserId: sender.UserId, - }) { - return false - } - } - - var data api.StringMap - if err := json.Unmarshal(message.Message.Data, &data); !c.assert.NoError(err) { - return false - } - - if dt, ok := api.GetStringMapEntry[string](data, "type"); !c.assert.True(ok, "no/invalid type in %+v", data) || - !c.assert.Equal("answer", dt, "invalid data type in %+v", data) { - return false - } - - if payload, ok := api.ConvertStringMap(data["payload"]); !c.assert.True(ok, "not a string map, got %+v", data["payload"]) { - return false - } else { - if pt, ok := api.GetStringMapEntry[string](payload, "type"); !c.assert.True(ok, "no/invalid type in payload %+v", payload) || - !c.assert.Equal("answer", pt, "invalid payload type in %+v", payload) { - return false - } - if sdp, ok := api.GetStringMapEntry[string](payload, "sdp"); !c.assert.True(ok, "no/invalid sdp in payload %+v", payload) || - !c.assert.Equal(answer, sdp, "invalid payload answer") { - return false - } - } - - return true -} - -func checkMessageTransientInitialOrSet(t *testing.T, message *api.ServerMessage, key string, value any) bool { - assert := assert.New(t) - return checkMessageType(t, message, "transient") && - assert.True(message.TransientData.Type == "initial" || message.TransientData.Type == "set", "invalid message type in %+v", message) && - assert.Equal(key, message.TransientData.Key, "invalid key in %+v", message) && - assert.EqualValues(value, message.TransientData.Value, "invalid value in %+v", message) && - assert.Nil(message.TransientData.OldValue, "invalid old value in %+v", message) -} - -func checkMessageTransientSet(t *testing.T, message *api.ServerMessage, key string, value any, oldValue any) bool { - assert := assert.New(t) - return checkMessageType(t, message, "transient") && - assert.Equal("set", message.TransientData.Type, "invalid message type in %+v", message) && - assert.Equal(key, message.TransientData.Key, "invalid key in %+v", message) && - assert.EqualValues(value, message.TransientData.Value, "invalid value in %+v", message) && - assert.EqualValues(oldValue, message.TransientData.OldValue, "invalid old value in %+v", message) -} - -func checkMessageTransientRemove(t *testing.T, message *api.ServerMessage, key string, oldValue any) bool { - assert := assert.New(t) - return checkMessageType(t, message, "transient") && - assert.Equal("remove", message.TransientData.Type, "invalid message type in %+v", message) && - assert.Equal(key, message.TransientData.Key, "invalid key in %+v", message) && - assert.EqualValues(oldValue, message.TransientData.OldValue, "invalid old value in %+v", message) -} - -func checkMessageTransientInitial(t *testing.T, message *api.ServerMessage, data api.StringMap) bool { - assert := assert.New(t) - return checkMessageType(t, message, "transient") && - assert.Equal("initial", message.TransientData.Type, "invalid message type in %+v", message) && - assert.Equal(data, message.TransientData.Data, "invalid initial data in %+v", message) -} - -func checkMessageInCallAll(t *testing.T, message *api.ServerMessage, roomId string, inCall int) bool { - assert := assert.New(t) - return checkMessageType(t, message, "event") && - assert.Equal("update", message.Event.Type, "invalid event type, got %+v", message.Event) && - assert.Equal("participants", message.Event.Target, "invalid event target, got %+v", message.Event) && - assert.Equal(roomId, message.Event.Update.RoomId, "invalid event update room id, got %+v", message.Event) && - assert.True(message.Event.Update.All, "expected participants update event for all, got %+v", message.Event) && - assert.EqualValues(strconv.FormatInt(int64(inCall), 10), message.Event.Update.InCall, "expected incall flags %d, got %+v", inCall, message.Event.Update) -} - -func checkMessageSwitchTo(t *testing.T, message *api.ServerMessage, roomId string, details json.RawMessage) (*api.EventServerMessageSwitchTo, bool) { - assert := assert.New(t) - if !checkMessageType(t, message, "event") || - !assert.Equal("switchto", message.Event.Type, "invalid event type, got %+v", message.Event) || - !assert.Equal("room", message.Event.Target, "invalid event target, got %+v", message.Event) || - !assert.Equal(roomId, message.Event.SwitchTo.RoomId, "invalid event switchto room id, got %+v", message.Event) { - return nil, false - } - if details != nil { - if !assert.NotEmpty(message.Event.SwitchTo.Details, "details missing in %+v", message) || - !assert.Equal(details, message.Event.SwitchTo.Details, "invalid details, got %+v", message.Event) { - return nil, false - } - } else if assert.Empty(message.Event.SwitchTo.Details, "expected no details in %+v", message) { - return nil, false - } - return message.Event.SwitchTo, true -} - -func (c *TestClient) RunUntilSwitchTo(ctx context.Context, roomId string, details json.RawMessage) (*api.EventServerMessageSwitchTo, bool) { - message, ok := c.RunUntilMessage(ctx) - if !ok { - return nil, false - } - - return checkMessageSwitchTo(c.t, message, roomId, details) -} diff --git a/server/testutils_test.go b/server/testutils_test.go deleted file mode 100644 index 6360d06..0000000 --- a/server/testutils_test.go +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "context" - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" -) - -func WaitForUsersJoined(ctx context.Context, t *testing.T, client1 *TestClient, hello1 *api.ServerMessage, client2 *TestClient, hello2 *api.ServerMessage) { - t.Helper() - // We will receive "joined" events for all clients. The ordering is not - // defined as messages are processed and sent by asynchronous event handlers. - client1.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) - client2.RunUntilJoined(ctx, hello1.Hello, hello2.Hello) -} - -func MustSucceed1[T any, A1 any](t *testing.T, f func(a1 A1) (T, bool), a1 A1) T { - t.Helper() - result, ok := f(a1) - if !ok { - t.FailNow() - } - return result -} - -func MustSucceed2[T any, A1 any, A2 any](t *testing.T, f func(a1 A1, a2 A2) (T, bool), a1 A1, a2 A2) T { - t.Helper() - result, ok := f(a1, a2) - if !ok { - t.FailNow() - } - return result -} - -func MustSucceed3[T any, A1 any, A2 any, A3 any](t *testing.T, f func(a1 A1, a2 A2, a3 A3) (T, bool), a1 A1, a2 A2, a3 A3) T { - t.Helper() - result, ok := f(a1, a2, a3) - if !ok { - t.FailNow() - } - return result -} - -func AssertEqualSerialized(t *testing.T, expected any, actual any, msgAndArgs ...any) bool { - t.Helper() - - e, err := json.MarshalIndent(expected, "", " ") - if !assert.NoError(t, err) { - return false - } - - a, err := json.MarshalIndent(actual, "", " ") - if !assert.NoError(t, err) { - return false - } - - return assert.Equal(t, string(a), string(e), msgAndArgs...) -} diff --git a/server/virtualsession_test.go b/server/virtualsession_test.go deleted file mode 100644 index 85181bb..0000000 --- a/server/virtualsession_test.go +++ /dev/null @@ -1,704 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2019 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package server - -import ( - "context" - "encoding/json" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -func TestVirtualSession(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - roomId := "the-room-id" - emptyProperties := json.RawMessage("{}") - backend := talk.NewCompatBackend(nil) - room, err := hub.CreateRoom(roomId, emptyProperties, backend) - require.NoError(err) - defer room.Close() - - clientInternal := NewTestClient(t, server, hub) - defer clientInternal.CloseWithBye() - require.NoError(clientInternal.SendHelloInternal()) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - if hello, ok := clientInternal.RunUntilHello(ctx); ok { - assert.Empty(hello.Hello.UserId) - assert.NotEmpty(hello.Hello.SessionId) - assert.NotEmpty(hello.Hello.ResumeId) - } - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - client.RunUntilJoined(ctx, hello.Hello) - - internalSessionId := api.PublicSessionId("session1") - userId := "user1" - msgAdd := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "addsession", - AddSession: &api.AddSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - UserId: userId, - Flags: FLAG_MUTED_SPEAKING, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgAdd)) - - msg1 := MustSucceed1(t, client.RunUntilMessage, ctx) - // The public session id will be generated by the server, so don't check for it. - require.True(client.checkMessageJoinedSession(msg1, "", userId)) - sessionId := msg1.Event.Join[0].SessionId - session := hub.GetSessionByPublicId(sessionId) - if assert.NotNil(session, "Could not get virtual session %s", sessionId) { - assert.Equal(api.HelloClientTypeVirtual, session.ClientType()) - sid := session.(*VirtualSession).SessionId() - assert.Equal(internalSessionId, sid) - } - - // Also a participants update event will be triggered for the virtual user. - msg2 := MustSucceed1(t, client.RunUntilMessage, ctx) - msg3 := MustSucceed1(t, client.RunUntilMessage, ctx) - if msg2.Type == "event" && msg3.Type == "event" && msg2.Event.Type == "flags" { - // The order is not specified, could be "participants" before "flags" or vice versa. - // Ensure consistent order for checks below ("participants", "flags"). - t.Logf("Switching messages order") - msg2, msg3 = msg3, msg2 - } - - if updateMsg, ok := checkMessageParticipantsInCall(t, msg2); ok { - assert.Equal(roomId, updateMsg.RoomId) - if assert.Len(updateMsg.Users, 1) { - assert.EqualValues(sessionId, updateMsg.Users[0]["sessionId"]) - assert.Equal(true, updateMsg.Users[0]["virtual"]) - assert.EqualValues((FlagInCall | FlagWithPhone), updateMsg.Users[0]["inCall"]) - } - } - - if flagsMsg, ok := checkMessageParticipantFlags(t, msg3); ok { - assert.Equal(roomId, flagsMsg.RoomId) - assert.Equal(sessionId, flagsMsg.SessionId) - assert.EqualValues(FLAG_MUTED_SPEAKING, flagsMsg.Flags) - } - - newFlags := uint32(FLAG_TALKING) - msgFlags := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "updatesession", - UpdateSession: &api.UpdateSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - Flags: &newFlags, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgFlags)) - - msg4 := MustSucceed1(t, client.RunUntilMessage, ctx) - if flagsMsg, ok := checkMessageParticipantFlags(t, msg4); ok { - assert.Equal(roomId, flagsMsg.RoomId) - assert.Equal(sessionId, flagsMsg.SessionId) - assert.Equal(newFlags, flagsMsg.Flags) - } - - // A new client will receive the initial flags of the virtual session. - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - gotFlags := false - var receivedMessages []*api.ServerMessage - for !gotFlags { - messages, err := client2.GetPendingMessages(ctx) - if err != nil { - assert.NoError(err) - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - break - } - } - - receivedMessages = append(receivedMessages, messages...) - for _, msg := range messages { - if msg.Type != "event" || msg.Event.Target != "participants" || msg.Event.Type != "flags" { - continue - } - - if assert.Equal(roomId, msg.Event.Flags.RoomId) && - assert.Equal(sessionId, msg.Event.Flags.SessionId) && - assert.Equal(newFlags, msg.Event.Flags.Flags) { - gotFlags = true - break - } - } - } - assert.True(gotFlags, "Didn't receive initial flags in %+v", receivedMessages) - - // Ignore "join" messages from second client - client.RunUntilJoined(ctx, hello2.Hello) - - // When sending to a virtual session, the message is sent to the actual - // client and contains a "Recipient" block with the internal session id. - recipient := api.MessageClientMessageRecipient{ - Type: "session", - SessionId: sessionId, - } - - data := "from-client-to-virtual" - require.NoError(client.SendMessage(recipient, data)) - - msg2 = MustSucceed1(t, clientInternal.RunUntilMessage, ctx) - require.True(checkMessageType(t, msg2, "message")) - require.True(checkMessageSender(t, hub, msg2.Message.Sender, "session", hello.Hello)) - - if assert.NotNil(msg2.Message.Recipient) { - assert.Equal("session", msg2.Message.Recipient.Type) - assert.Equal(internalSessionId, msg2.Message.Recipient.SessionId) - } - - var payload string - if err := json.Unmarshal(msg2.Message.Data, &payload); assert.NoError(err) { - assert.Equal(data, payload) - } - - msgRemove := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "removesession", - RemoveSession: &api.RemoveSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgRemove)) - - if msg5, ok := client.RunUntilMessage(ctx); ok { - client.checkMessageRoomLeaveSession(msg5, sessionId) - } -} - -func TestVirtualSessionActorInformation(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - roomId := "the-room-id" - emptyProperties := json.RawMessage("{}") - backend := talk.NewCompatBackend(nil) - room, err := hub.CreateRoom(roomId, emptyProperties, backend) - require.NoError(err) - defer room.Close() - - clientInternal := NewTestClient(t, server, hub) - defer clientInternal.CloseWithBye() - require.NoError(clientInternal.SendHelloInternal()) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - if hello, ok := clientInternal.RunUntilHello(ctx); ok { - assert.Empty(hello.Hello.UserId) - assert.NotEmpty(hello.Hello.SessionId) - assert.NotEmpty(hello.Hello.ResumeId) - } - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // Ignore "join" events. - client.RunUntilJoined(ctx, hello.Hello) - - internalSessionId := api.PublicSessionId("session1") - userId := "user1" - msgAdd := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "addsession", - AddSession: &api.AddSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - UserId: userId, - Flags: FLAG_MUTED_SPEAKING, - Options: &api.AddSessionOptions{ - ActorId: "actor-id", - ActorType: "actor-type", - }, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgAdd)) - - msg1 := MustSucceed1(t, client.RunUntilMessage, ctx) - // The public session id will be generated by the server, so don't check for it. - require.True(client.checkMessageJoinedSession(msg1, "", userId)) - sessionId := msg1.Event.Join[0].SessionId - session := hub.GetSessionByPublicId(sessionId) - if assert.NotNil(session, "Could not get virtual session %s", sessionId) { - assert.Equal(api.HelloClientTypeVirtual, session.ClientType()) - sid := session.(*VirtualSession).SessionId() - assert.Equal(internalSessionId, sid) - } - - // Also a participants update event will be triggered for the virtual user. - msg2 := MustSucceed1(t, client.RunUntilMessage, ctx) - if updateMsg, ok := checkMessageParticipantsInCall(t, msg2); ok { - assert.Equal(roomId, updateMsg.RoomId) - if assert.Len(updateMsg.Users, 1) { - assert.EqualValues(sessionId, updateMsg.Users[0]["sessionId"]) - assert.Equal(true, updateMsg.Users[0]["virtual"]) - assert.EqualValues((FlagInCall | FlagWithPhone), updateMsg.Users[0]["inCall"]) - } - } - - msg3 := MustSucceed1(t, client.RunUntilMessage, ctx) - if flagsMsg, ok := checkMessageParticipantFlags(t, msg3); ok { - assert.Equal(roomId, flagsMsg.RoomId) - assert.Equal(sessionId, flagsMsg.SessionId) - assert.EqualValues(FLAG_MUTED_SPEAKING, flagsMsg.Flags) - } - - newFlags := uint32(FLAG_TALKING) - msgFlags := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "updatesession", - UpdateSession: &api.UpdateSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - Flags: &newFlags, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgFlags)) - - msg4 := MustSucceed1(t, client.RunUntilMessage, ctx) - if flagsMsg, ok := checkMessageParticipantFlags(t, msg4); ok { - assert.Equal(roomId, flagsMsg.RoomId) - assert.Equal(sessionId, flagsMsg.SessionId) - assert.Equal(newFlags, flagsMsg.Flags) - } - - // A new client will receive the initial flags of the virtual session. - client2, hello2 := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId+"2") - roomMsg = MustSucceed2(t, client2.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - gotFlags := false - var receivedMessages []*api.ServerMessage - for !gotFlags { - messages, err := client2.GetPendingMessages(ctx) - if err != nil { - assert.NoError(err) - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - break - } - } - - receivedMessages = append(receivedMessages, messages...) - for _, msg := range messages { - if msg.Type != "event" || msg.Event.Target != "participants" || msg.Event.Type != "flags" { - continue - } - - if assert.Equal(roomId, msg.Event.Flags.RoomId) && - assert.Equal(sessionId, msg.Event.Flags.SessionId) && - assert.Equal(newFlags, msg.Event.Flags.Flags) { - gotFlags = true - break - } - } - } - assert.True(gotFlags, "Didn't receive initial flags in %+v", receivedMessages) - - // Ignore "join" messages from second client - client.RunUntilJoined(ctx, hello2.Hello) - - // When sending to a virtual session, the message is sent to the actual - // client and contains a "Recipient" block with the internal session id. - recipient := api.MessageClientMessageRecipient{ - Type: "session", - SessionId: sessionId, - } - - data := "from-client-to-virtual" - require.NoError(client.SendMessage(recipient, data)) - - msg2 = MustSucceed1(t, clientInternal.RunUntilMessage, ctx) - require.True(checkMessageType(t, msg2, "message")) - require.True(checkMessageSender(t, hub, msg2.Message.Sender, "session", hello.Hello)) - - if assert.NotNil(msg2.Message.Recipient) { - assert.Equal("session", msg2.Message.Recipient.Type) - assert.Equal(internalSessionId, msg2.Message.Recipient.SessionId) - } - - var payload string - if err := json.Unmarshal(msg2.Message.Data, &payload); assert.NoError(err) { - assert.Equal(data, payload) - } - - msgRemove := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "removesession", - RemoveSession: &api.RemoveSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgRemove)) - - if msg5, ok := client.RunUntilMessage(ctx); ok { - client.checkMessageRoomLeaveSession(msg5, sessionId) - } -} - -func checkHasEntryWithInCall(t *testing.T, message *api.RoomEventServerMessage, sessionId api.PublicSessionId, entryType string, inCall int) bool { - assert := assert.New(t) - found := false - for _, entry := range message.Users { - if sid, ok := api.GetStringMapString[api.PublicSessionId](entry, "sessionId"); ok && sid == sessionId { - if value, found := api.GetStringMapEntry[bool](entry, entryType); !assert.True(found, "entry %s not found or invalid in %+v", entryType, entry) || - !assert.True(value, "entry %s invalid in %+v", entryType, entry) { - return false - } - - if value, found := api.GetStringMapEntry[float64](entry, "inCall"); !assert.True(found, "inCall not found or invalid in %+v", entry) || - !assert.InDelta(value, inCall, 0.0001, "invalid inCall") { - return false - } - found = true - break - } - } - - return assert.True(found, "no user with session id %s found, got %+v", sessionId, message) -} - -func TestVirtualSessionCustomInCall(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - roomId := "the-room-id" - emptyProperties := json.RawMessage("{}") - backend := talk.NewCompatBackend(nil) - room, err := hub.CreateRoom(roomId, emptyProperties, backend) - require.NoError(err) - defer room.Close() - - clientInternal := NewTestClient(t, server, hub) - defer clientInternal.CloseWithBye() - features := []string{ - api.ClientFeatureInternalInCall, - } - require.NoError(clientInternal.SendHelloInternalWithFeatures(features)) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - helloInternal, ok := clientInternal.RunUntilHello(ctx) - if ok { - assert.Empty(helloInternal.Hello.UserId) - assert.NotEmpty(helloInternal.Hello.SessionId) - assert.NotEmpty(helloInternal.Hello.ResumeId) - } - roomMsg := MustSucceed3(t, clientInternal.JoinRoomWithRoomSession, ctx, roomId, "") - require.Equal(roomId, roomMsg.Room.RoomId) - - roomMsg = MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // In some cases, the participants update event is triggered a bit after the joined - // event. If this happens, the "client" will also receive an additional update - // event after the joined of the internal client. - var expectUpdate bool - if _, additional, ok := clientInternal.RunUntilJoinedAndReturn(ctx, helloInternal.Hello, hello.Hello); ok { - if len(additional) == 0 { - if msg, ok := clientInternal.RunUntilMessage(ctx); ok { - additional = append(additional, msg) - } - expectUpdate = true - } - if assert.Len(additional, 1) && assert.Equal("event", additional[0].Type) { - assert.Equal("participants", additional[0].Event.Target) - assert.Equal("update", additional[0].Event.Type) - assert.EqualValues(helloInternal.Hello.SessionId, additional[0].Event.Update.Users[0]["sessionId"]) - assert.EqualValues(0, additional[0].Event.Update.Users[0]["inCall"]) - } - } - if _, additional, ok := client.RunUntilJoinedAndReturn(ctx, helloInternal.Hello, hello.Hello); ok { - if expectUpdate { - if len(additional) == 0 { - if msg, ok := client.RunUntilMessage(ctx); ok { - additional = append(additional, msg) - } - } - - if assert.Len(additional, 1) && assert.Equal("event", additional[0].Type) { - assert.Equal("participants", additional[0].Event.Target) - assert.Equal("update", additional[0].Event.Type) - assert.EqualValues(helloInternal.Hello.SessionId, additional[0].Event.Update.Users[0]["sessionId"]) - assert.EqualValues(0, additional[0].Event.Update.Users[0]["inCall"]) - } - } - } - - internalSessionId := api.PublicSessionId("session1") - userId := "user1" - msgAdd := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "addsession", - AddSession: &api.AddSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - UserId: userId, - Flags: FLAG_MUTED_SPEAKING, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgAdd)) - - msg1 := MustSucceed1(t, client.RunUntilMessage, ctx) - // The public session id will be generated by the server, so don't check for it. - require.True(client.checkMessageJoinedSession(msg1, "", userId)) - sessionId := msg1.Event.Join[0].SessionId - session := hub.GetSessionByPublicId(sessionId) - if assert.NotNil(session) { - assert.Equal(api.HelloClientTypeVirtual, session.ClientType()) - sid := session.(*VirtualSession).SessionId() - assert.Equal(internalSessionId, sid) - } - - // Also a participants update event will be triggered for the virtual user. - msg2 := MustSucceed1(t, client.RunUntilMessage, ctx) - if updateMsg, ok := checkMessageParticipantsInCall(t, msg2); ok { - assert.Equal(roomId, updateMsg.RoomId) - assert.Len(updateMsg.Users, 2) - - checkHasEntryWithInCall(t, updateMsg, sessionId, "virtual", 0) - checkHasEntryWithInCall(t, updateMsg, helloInternal.Hello.SessionId, "internal", 0) - } - - msg3 := MustSucceed1(t, client.RunUntilMessage, ctx) - if flagsMsg, ok := checkMessageParticipantFlags(t, msg3); ok { - assert.Equal(roomId, flagsMsg.RoomId) - assert.Equal(sessionId, flagsMsg.SessionId) - assert.EqualValues(FLAG_MUTED_SPEAKING, flagsMsg.Flags) - } - - // The internal session can change its "inCall" flags - msgInCall := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "incall", - InCall: &api.InCallInternalClientMessage{ - InCall: FlagInCall | FlagWithAudio, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgInCall)) - - msg4 := MustSucceed1(t, client.RunUntilMessage, ctx) - if updateMsg, ok := checkMessageParticipantsInCall(t, msg4); ok { - assert.Equal(roomId, updateMsg.RoomId) - assert.Len(updateMsg.Users, 2) - checkHasEntryWithInCall(t, updateMsg, sessionId, "virtual", 0) - checkHasEntryWithInCall(t, updateMsg, helloInternal.Hello.SessionId, "internal", FlagInCall|FlagWithAudio) - } - - // The internal session can change the "inCall" flags of a virtual session - newInCall := FlagInCall | FlagWithPhone - msgInCall2 := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "updatesession", - UpdateSession: &api.UpdateSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - InCall: &newInCall, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgInCall2)) - - msg5 := MustSucceed1(t, client.RunUntilMessage, ctx) - if updateMsg, ok := checkMessageParticipantsInCall(t, msg5); ok { - assert.Equal(roomId, updateMsg.RoomId) - assert.Len(updateMsg.Users, 2) - checkHasEntryWithInCall(t, updateMsg, sessionId, "virtual", newInCall) - checkHasEntryWithInCall(t, updateMsg, helloInternal.Hello.SessionId, "internal", FlagInCall|FlagWithAudio) - } - - newInCall2 := FlagDisconnected - msgInCall3 := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "updatesession", - UpdateSession: &api.UpdateSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - InCall: &newInCall2, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgInCall3)) - - msg6 := MustSucceed1(t, client.RunUntilMessage, ctx) - if updateMsg, ok := checkMessageParticipantsInCall(t, msg6); ok { - assert.Equal(roomId, updateMsg.RoomId) - assert.Len(updateMsg.Users, 2) - checkHasEntryWithInCall(t, updateMsg, sessionId, "virtual", newInCall2) - checkHasEntryWithInCall(t, updateMsg, helloInternal.Hello.SessionId, "internal", FlagInCall|FlagWithAudio) - } -} - -func TestVirtualSessionCleanup(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - hub, _, _, server := CreateHubForTest(t) - - roomId := "the-room-id" - emptyProperties := json.RawMessage("{}") - backend := talk.NewCompatBackend(nil) - room, err := hub.CreateRoom(roomId, emptyProperties, backend) - require.NoError(err) - defer room.Close() - - clientInternal := NewTestClient(t, server, hub) - defer clientInternal.CloseWithBye() - require.NoError(clientInternal.SendHelloInternal()) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - if hello, ok := clientInternal.RunUntilHello(ctx); ok { - assert.Empty(hello.Hello.UserId) - assert.NotEmpty(hello.Hello.SessionId) - assert.NotEmpty(hello.Hello.ResumeId) - } - client, hello := NewTestClientWithHello(ctx, t, server, hub, testDefaultUserId) - - roomMsg := MustSucceed2(t, client.JoinRoom, ctx, roomId) - require.Equal(roomId, roomMsg.Room.RoomId) - - // Ignore "join" events. - client.RunUntilJoined(ctx, hello.Hello) - - internalSessionId := api.PublicSessionId("session1") - userId := "user1" - msgAdd := &api.ClientMessage{ - Type: "internal", - Internal: &api.InternalClientMessage{ - Type: "addsession", - AddSession: &api.AddSessionInternalClientMessage{ - CommonSessionInternalClientMessage: api.CommonSessionInternalClientMessage{ - SessionId: internalSessionId, - RoomId: roomId, - }, - UserId: userId, - Flags: FLAG_MUTED_SPEAKING, - }, - }, - } - require.NoError(clientInternal.WriteJSON(msgAdd)) - - msg1 := MustSucceed1(t, client.RunUntilMessage, ctx) - // The public session id will be generated by the server, so don't check for it. - require.True(client.checkMessageJoinedSession(msg1, "", userId)) - sessionId := msg1.Event.Join[0].SessionId - session := hub.GetSessionByPublicId(sessionId) - if assert.NotNil(session) { - assert.Equal(api.HelloClientTypeVirtual, session.ClientType()) - sid := session.(*VirtualSession).SessionId() - assert.Equal(internalSessionId, sid) - } - - // Also a participants update event will be triggered for the virtual user. - msg2 := MustSucceed1(t, client.RunUntilMessage, ctx) - if updateMsg, ok := checkMessageParticipantsInCall(t, msg2); ok { - assert.Equal(roomId, updateMsg.RoomId) - if assert.Len(updateMsg.Users, 1) { - assert.EqualValues(sessionId, updateMsg.Users[0]["sessionId"]) - assert.Equal(true, updateMsg.Users[0]["virtual"]) - assert.EqualValues((FlagInCall | FlagWithPhone), updateMsg.Users[0]["inCall"]) - } - } - - msg3 := MustSucceed1(t, client.RunUntilMessage, ctx) - if flagsMsg, ok := checkMessageParticipantFlags(t, msg3); ok { - assert.Equal(roomId, flagsMsg.RoomId) - assert.Equal(sessionId, flagsMsg.SessionId) - assert.EqualValues(FLAG_MUTED_SPEAKING, flagsMsg.Flags) - } - - // The virtual sessions are closed when the parent session is deleted. - clientInternal.CloseWithBye() - - if msg2, ok := client.RunUntilMessage(ctx); ok { - client.checkMessageRoomLeaveSession(msg2, sessionId) - } -} diff --git a/server/session.go b/session.go similarity index 51% rename from server/session.go rename to session.go index 0a76ed3..d08b8ec 100644 --- a/server/session.go +++ b/session.go @@ -19,59 +19,68 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" "encoding/json" "net/url" "sync" - "time" +) - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/session" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" +type Permission string + +var ( + PERMISSION_MAY_PUBLISH_MEDIA Permission = "publish-media" + PERMISSION_MAY_PUBLISH_AUDIO Permission = "publish-audio" + PERMISSION_MAY_PUBLISH_VIDEO Permission = "publish-video" + PERMISSION_MAY_PUBLISH_SCREEN Permission = "publish-screen" + PERMISSION_MAY_CONTROL Permission = "control" + PERMISSION_TRANSIENT_DATA Permission = "transient-data" + PERMISSION_HIDE_DISPLAYNAMES Permission = "hide-displaynames" + + // DefaultPermissionOverrides contains permission overrides for users where + // no permissions have been set by the server. If a permission is not set in + // this map, it's assumed the user has that permission. + DefaultPermissionOverrides = map[Permission]bool{ + PERMISSION_HIDE_DISPLAYNAMES: false, + } ) type Session interface { Context() context.Context - PrivateId() api.PrivateSessionId - PublicId() api.PublicSessionId - ClientType() api.ClientType - Data() *session.SessionIdData + PrivateId() string + PublicId() string + ClientType() string + Data() *SessionIdData UserId() string UserData() json.RawMessage - ParsedUserData() (api.StringMap, error) + ParsedUserData() (map[string]interface{}, error) - Backend() *talk.Backend + Backend() *Backend BackendUrl() string ParsedBackendUrl() *url.URL - SetRoom(room *Room, joinTime time.Time) + SetRoom(room *Room) GetRoom() *Room LeaveRoom(notify bool) *Room - IsInRoom(id string) bool Close() - HasPermission(permission api.Permission) bool + HasPermission(permission Permission) bool - SendError(e *api.Error) bool - SendMessage(message *api.ServerMessage) bool + SendError(e *Error) bool + SendMessage(message *ServerMessage) bool } -type SessionWithInCall interface { - GetInCall() int -} - -func parseUserData(data json.RawMessage) func() (api.StringMap, error) { - return sync.OnceValues(func() (api.StringMap, error) { +func parseUserData(data json.RawMessage) func() (map[string]interface{}, error) { + return sync.OnceValues(func() (map[string]interface{}, error) { if len(data) == 0 { return nil, nil } - var m api.StringMap + var m map[string]interface{} if err := json.Unmarshal(data, &m); err != nil { return nil, err } diff --git a/session.pb.go b/session.pb.go new file mode 100644 index 0000000..f875cbf --- /dev/null +++ b/session.pb.go @@ -0,0 +1,172 @@ +//* +// Standalone signaling server for the Nextcloud Spreed app. +// Copyright (C) 2024 struktur AG +// +// @author Joachim Bauch +// +// @license GNU AGPL version 3 or any later version +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: session.proto + +package signaling + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SessionIdData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sid uint64 `protobuf:"varint,1,opt,name=Sid,proto3" json:"Sid,omitempty"` + Created *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=Created,proto3" json:"Created,omitempty"` + BackendId string `protobuf:"bytes,3,opt,name=BackendId,proto3" json:"BackendId,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionIdData) Reset() { + *x = SessionIdData{} + mi := &file_session_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionIdData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionIdData) ProtoMessage() {} + +func (x *SessionIdData) ProtoReflect() protoreflect.Message { + mi := &file_session_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionIdData.ProtoReflect.Descriptor instead. +func (*SessionIdData) Descriptor() ([]byte, []int) { + return file_session_proto_rawDescGZIP(), []int{0} +} + +func (x *SessionIdData) GetSid() uint64 { + if x != nil { + return x.Sid + } + return 0 +} + +func (x *SessionIdData) GetCreated() *timestamppb.Timestamp { + if x != nil { + return x.Created + } + return nil +} + +func (x *SessionIdData) GetBackendId() string { + if x != nil { + return x.BackendId + } + return "" +} + +var File_session_proto protoreflect.FileDescriptor + +var file_session_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x75, 0x0a, 0x0d, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, + 0x53, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x53, 0x69, 0x64, 0x12, 0x34, + 0x0a, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, + 0x49, 0x64, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x73, 0x74, 0x72, 0x75, 0x6b, 0x74, 0x75, 0x72, 0x61, 0x67, 0x2f, 0x6e, 0x65, 0x78, 0x74, + 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x70, 0x72, 0x65, 0x65, 0x64, 0x2d, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x3b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x69, 0x6e, 0x67, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_session_proto_rawDescOnce sync.Once + file_session_proto_rawDescData = file_session_proto_rawDesc +) + +func file_session_proto_rawDescGZIP() []byte { + file_session_proto_rawDescOnce.Do(func() { + file_session_proto_rawDescData = protoimpl.X.CompressGZIP(file_session_proto_rawDescData) + }) + return file_session_proto_rawDescData +} + +var file_session_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_session_proto_goTypes = []any{ + (*SessionIdData)(nil), // 0: signaling.SessionIdData + (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp +} +var file_session_proto_depIdxs = []int32{ + 1, // 0: signaling.SessionIdData.Created:type_name -> google.protobuf.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_session_proto_init() } +func file_session_proto_init() { + if File_session_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_session_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_session_proto_goTypes, + DependencyIndexes: file_session_proto_depIdxs, + MessageInfos: file_session_proto_msgTypes, + }.Build() + File_session_proto = out.File + file_session_proto_rawDesc = nil + file_session_proto_goTypes = nil + file_session_proto_depIdxs = nil +} diff --git a/session/session.proto b/session.proto similarity index 89% rename from session/session.proto rename to session.proto index 6f976b2..4b1cc71 100644 --- a/session/session.proto +++ b/session.proto @@ -21,12 +21,14 @@ */ syntax = "proto3"; -option go_package = "github.com/strukturag/nextcloud-spreed-signaling/session"; +import "google/protobuf/timestamp.proto"; -package session; +option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling"; + +package signaling; message SessionIdData { uint64 Sid = 1; - int64 Created = 2; + google.protobuf.Timestamp Created = 2; string BackendId = 3; } diff --git a/session/session.pb.go b/session/session.pb.go deleted file mode 100644 index 0bf63ee..0000000 --- a/session/session.pb.go +++ /dev/null @@ -1,158 +0,0 @@ -//* -// Standalone signaling server for the Nextcloud Spreed app. -// Copyright (C) 2024 struktur AG -// -// @author Joachim Bauch -// -// @license GNU AGPL version 3 or any later version -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: session/session.proto - -package session - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type SessionIdData struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sid uint64 `protobuf:"varint,1,opt,name=Sid,proto3" json:"Sid,omitempty"` - Created int64 `protobuf:"varint,2,opt,name=Created,proto3" json:"Created,omitempty"` - BackendId string `protobuf:"bytes,3,opt,name=BackendId,proto3" json:"BackendId,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SessionIdData) Reset() { - *x = SessionIdData{} - mi := &file_session_session_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SessionIdData) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SessionIdData) ProtoMessage() {} - -func (x *SessionIdData) ProtoReflect() protoreflect.Message { - mi := &file_session_session_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SessionIdData.ProtoReflect.Descriptor instead. -func (*SessionIdData) Descriptor() ([]byte, []int) { - return file_session_session_proto_rawDescGZIP(), []int{0} -} - -func (x *SessionIdData) GetSid() uint64 { - if x != nil { - return x.Sid - } - return 0 -} - -func (x *SessionIdData) GetCreated() int64 { - if x != nil { - return x.Created - } - return 0 -} - -func (x *SessionIdData) GetBackendId() string { - if x != nil { - return x.BackendId - } - return "" -} - -var File_session_session_proto protoreflect.FileDescriptor - -const file_session_session_proto_rawDesc = "" + - "\n" + - "\x15session/session.proto\x12\asession\"Y\n" + - "\rSessionIdData\x12\x10\n" + - "\x03Sid\x18\x01 \x01(\x04R\x03Sid\x12\x18\n" + - "\aCreated\x18\x02 \x01(\x03R\aCreated\x12\x1c\n" + - "\tBackendId\x18\x03 \x01(\tR\tBackendIdB:Z8github.com/strukturag/nextcloud-spreed-signaling/sessionb\x06proto3" - -var ( - file_session_session_proto_rawDescOnce sync.Once - file_session_session_proto_rawDescData []byte -) - -func file_session_session_proto_rawDescGZIP() []byte { - file_session_session_proto_rawDescOnce.Do(func() { - file_session_session_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_session_session_proto_rawDesc), len(file_session_session_proto_rawDesc))) - }) - return file_session_session_proto_rawDescData -} - -var file_session_session_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_session_session_proto_goTypes = []any{ - (*SessionIdData)(nil), // 0: session.SessionIdData -} -var file_session_session_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_session_session_proto_init() } -func file_session_session_proto_init() { - if File_session_session_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_session_session_proto_rawDesc), len(file_session_session_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_session_session_proto_goTypes, - DependencyIndexes: file_session_session_proto_depIdxs, - MessageInfos: file_session_session_proto_msgTypes, - }.Build() - File_session_session_proto = out.File - file_session_session_proto_goTypes = nil - file_session_session_proto_depIdxs = nil -} diff --git a/session/sessionid_codec.go b/session/sessionid_codec.go deleted file mode 100644 index 3e2c576..0000000 --- a/session/sessionid_codec.go +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2024 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package session - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/hmac" - "crypto/rand" - "crypto/sha256" - "crypto/subtle" - "encoding/base64" - "errors" - "fmt" - "hash" - "io" - "sync" - "unsafe" - - "google.golang.org/protobuf/proto" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" -) - -const ( - privateSessionName = "private-session" - publicSessionName = "public-session" - - // hmacLength specifies the length of the HMAC to use. 80 bits should be enough - // to prevent tampering. - hmacLength = 10 -) - -var ( - sessionHashFunc = sha256.New - sessionEncoding = base64.URLEncoding.WithPadding(base64.NoPadding) - sessionMarshalOptions = proto.MarshalOptions{ - UseCachedSize: true, - } - sessionUnmarshalOptions = proto.UnmarshalOptions{} - sessionSeparator = []byte{'|'} -) - -type bytesPool struct { - pool sync.Pool -} - -func (p *bytesPool) Get(size int) []byte { - bb := p.pool.Get() - if bb == nil { - return make([]byte, size) - } - - b := *(bb.(*[]byte)) - if cap(b) < size { - b = make([]byte, size) - } else { - b = b[:size] - } - return b -} - -func (p *bytesPool) Put(b []byte) { - p.pool.Put(&b) -} - -// SessionIdCodec encodes and decodes session ids. -// -// Inspired by https://github.com/gorilla/securecookie -type SessionIdCodec struct { // nolint - hashKey []byte - cipher cipher.Block - - bytesPool bytesPool - hmacPool sync.Pool - dataPool sync.Pool -} - -func NewSessionIdCodec(hashKey []byte, blockKey []byte) (*SessionIdCodec, error) { - if len(hashKey) == 0 { - return nil, errors.New("hash key is not set") - } - - codec := &SessionIdCodec{ - hashKey: hashKey, - hmacPool: sync.Pool{ - New: func() any { - return hmac.New(sessionHashFunc, hashKey) - }, - }, - dataPool: sync.Pool{ - New: func() any { - return &SessionIdData{} - }, - }, - } - if len(blockKey) > 0 { - block, err := aes.NewCipher(blockKey) - if err != nil { - return nil, fmt.Errorf("error creating cipher: %w", err) - } - codec.cipher = block - } - return codec, nil -} - -func (c *SessionIdCodec) encrypt(data []byte) ([]byte, error) { - iv := c.bytesPool.Get(c.cipher.BlockSize() + len(data))[:c.cipher.BlockSize()] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, fmt.Errorf("error creating iv: %w", err) - } - - ctr := cipher.NewCTR(c.cipher, iv) - ctr.XORKeyStream(data, data) - return append(iv, data...), nil -} - -func (c *SessionIdCodec) decrypt(data []byte) ([]byte, error) { - bs := c.cipher.BlockSize() - if len(data) <= bs { - return nil, errors.New("no iv found in data") - } - - iv := data[:bs] - data = data[bs:] - ctr := cipher.NewCTR(c.cipher, iv) - ctr.XORKeyStream(data, data) - return data, nil -} - -func (c *SessionIdCodec) encodeToString(b []byte) string { - s := c.bytesPool.Get(sessionEncoding.EncodedLen(len(b))) - defer c.bytesPool.Put(s) - - sessionEncoding.Encode(s, b) - return string(s) -} - -func (c *SessionIdCodec) decodeFromString(s string) ([]byte, error) { - b := c.bytesPool.Get(sessionEncoding.DecodedLen(len(s))) - n, err := sessionEncoding.Decode(b, []byte(s)) - if err != nil { - c.bytesPool.Put(b) - return nil, err - } - - return b[:n], nil -} - -func (c *SessionIdCodec) encodeRaw(name string, data *SessionIdData) ([]byte, error) { - body := c.bytesPool.Get(sessionMarshalOptions.Size(data)) - defer c.bytesPool.Put(body) - - body, err := sessionMarshalOptions.MarshalAppend(body[:0], data) - if err != nil { - return nil, fmt.Errorf("error marshaling data: %w", err) - } - - if c.cipher != nil { - body, err = c.encrypt(body) - if err != nil { - return nil, fmt.Errorf("error encrypting data: %w", err) - } - - defer c.bytesPool.Put(body) - } - - h := c.hmacPool.Get().(hash.Hash) - defer c.hmacPool.Put(h) - h.Reset() - h.Write(unsafe.Slice(unsafe.StringData(name), len(name))) // nolint - h.Write(sessionSeparator) // nolint - h.Write(body) // nolint - mac := c.bytesPool.Get(h.Size()) - defer c.bytesPool.Put(mac) - mac = h.Sum(mac[:0]) - - result := c.bytesPool.Get(len(body) + hmacLength)[:0] - result = append(result, body...) - result = append(result, mac[:hmacLength]...) - return result, nil -} - -func (c *SessionIdCodec) decodeRaw(name string, value []byte) (*SessionIdData, error) { - h := c.hmacPool.Get().(hash.Hash) - defer c.hmacPool.Put(h) - size := min(hmacLength, h.Size()) - if len(value) <= size { - return nil, errors.New("no hmac found in session id") - } - - h.Reset() - mac := value[len(value)-size:] - decoded := value[:len(value)-size] - - h.Write(unsafe.Slice(unsafe.StringData(name), len(name))) // nolint - h.Write(sessionSeparator) // nolint - h.Write(decoded) // nolint - check := c.bytesPool.Get(h.Size()) - defer c.bytesPool.Put(check) - if subtle.ConstantTimeCompare(mac, h.Sum(check[:0])[:hmacLength]) == 0 { - return nil, errors.New("invalid hmac in session id") - } - - if c.cipher != nil { - var err error - if decoded, err = c.decrypt(decoded); err != nil { - return nil, fmt.Errorf("invalid session id: %w", err) - } - } - - data := c.dataPool.Get().(*SessionIdData) - if err := sessionUnmarshalOptions.Unmarshal(decoded, data); err != nil { - c.dataPool.Put(data) - return nil, fmt.Errorf("invalid session id: %w", err) - } - - return data, nil -} - -func (c *SessionIdCodec) EncodePrivate(sessionData *SessionIdData) (api.PrivateSessionId, error) { - id, err := c.encodeRaw(privateSessionName, sessionData) - if err != nil { - return "", err - } - - defer c.bytesPool.Put(id) - return api.PrivateSessionId(c.encodeToString(id)), nil -} - -func (c *SessionIdCodec) reverseSessionId(data []byte) { - for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { - data[i], data[j] = data[j], data[i] - } -} - -func (c *SessionIdCodec) EncodePublic(sessionData *SessionIdData) (api.PublicSessionId, error) { - encoded, err := c.encodeRaw(publicSessionName, sessionData) - if err != nil { - return "", err - } - - // We are reversing the public session ids because clients compare them - // to decide who calls whom. The prefix of the session id is increasing - // (a timestamp) but the suffix the (random) hash. - // By reversing we move the hash to the front, making the comparison of - // session ids "random". - c.reverseSessionId(encoded) - - defer c.bytesPool.Put(encoded) - return api.PublicSessionId(c.encodeToString(encoded)), nil -} - -func (c *SessionIdCodec) DecodePrivate(encodedData api.PrivateSessionId) (*SessionIdData, error) { - decoded, err := c.decodeFromString(string(encodedData)) - if err != nil { - return nil, fmt.Errorf("invalid session id: %w", err) - } - defer c.bytesPool.Put(decoded) - - return c.decodeRaw(privateSessionName, decoded) -} - -func (c *SessionIdCodec) DecodePublic(encodedData api.PublicSessionId) (*SessionIdData, error) { - decoded, err := c.decodeFromString(string(encodedData)) - if err != nil { - return nil, fmt.Errorf("invalid session id: %w", err) - } - defer c.bytesPool.Put(decoded) - - c.reverseSessionId(decoded) - return c.decodeRaw(publicSessionName, decoded) -} - -func (c *SessionIdCodec) Put(data *SessionIdData) { - if data != nil { - data.Reset() - c.dataPool.Put(data) - } -} diff --git a/session/sessionid_codec_test.go b/session/sessionid_codec_test.go deleted file mode 100644 index 3e5fb4b..0000000 --- a/session/sessionid_codec_test.go +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2024 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package session - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -func TestReverseSessionId(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - codec, err := NewSessionIdCodec([]byte("12345678901234567890123456789012"), []byte("09876543210987654321098765432109")) - require.NoError(err) - a := []byte("12345") - codec.reverseSessionId(a) - assert.Equal([]byte("54321"), a) - b := []byte("4321") - codec.reverseSessionId(b) - assert.Equal([]byte("1234"), b) -} - -func Benchmark_EncodePrivateSessionId(b *testing.B) { - require := require.New(b) - backend := talk.NewCompatBackend(nil) - data := &SessionIdData{ - Sid: 1, - Created: time.Now().UnixMicro(), - BackendId: backend.Id(), - } - codec, err := NewSessionIdCodec([]byte("12345678901234567890123456789012"), []byte("09876543210987654321098765432109")) - require.NoError(err) - for b.Loop() { - if _, err := codec.EncodePrivate(data); err != nil { - b.Fatal(err) - } - } -} - -func Benchmark_DecodePrivateSessionId(b *testing.B) { - require := require.New(b) - backend := talk.NewCompatBackend(nil) - data := &SessionIdData{ - Sid: 1, - Created: time.Now().UnixMicro(), - BackendId: backend.Id(), - } - codec, err := NewSessionIdCodec([]byte("12345678901234567890123456789012"), []byte("09876543210987654321098765432109")) - require.NoError(err) - sid, err := codec.EncodePrivate(data) - require.NoError(err) - for b.Loop() { - if decoded, err := codec.DecodePrivate(sid); err != nil { - b.Fatal(err) - } else { - codec.Put(decoded) - } - } -} - -func Benchmark_EncodePublicSessionId(b *testing.B) { - require := require.New(b) - backend := talk.NewCompatBackend(nil) - data := &SessionIdData{ - Sid: 1, - Created: time.Now().UnixMicro(), - BackendId: backend.Id(), - } - codec, err := NewSessionIdCodec([]byte("12345678901234567890123456789012"), []byte("09876543210987654321098765432109")) - require.NoError(err) - for b.Loop() { - if _, err := codec.EncodePublic(data); err != nil { - b.Fatal(err) - } - } -} - -func Benchmark_DecodePublicSessionId(b *testing.B) { - require := require.New(b) - backend := talk.NewCompatBackend(nil) - data := &SessionIdData{ - Sid: 1, - Created: time.Now().UnixMicro(), - BackendId: backend.Id(), - } - codec, err := NewSessionIdCodec([]byte("12345678901234567890123456789012"), []byte("09876543210987654321098765432109")) - require.NoError(err) - sid, err := codec.EncodePublic(data) - require.NoError(err) - for b.Loop() { - if decoded, err := codec.DecodePublic(sid); err != nil { - b.Fatal(err) - } else { - codec.Put(decoded) - } - } -} - -func TestPublicPrivate(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - sd := &SessionIdData{ - Sid: 1, - Created: time.Now().UnixMicro(), - BackendId: "foo", - } - - codec, err := NewSessionIdCodec([]byte("0123456789012345"), []byte("0123456789012345")) - require.NoError(err) - private, err := codec.EncodePrivate(sd) - require.NoError(err) - public, err := codec.EncodePublic(sd) - require.NoError(err) - assert.NotEqual(private, public) - - if data, err := codec.DecodePublic(public); assert.NoError(err) { - assert.Equal(sd.Sid, data.Sid) - assert.Equal(sd.Created, data.Created) - assert.Equal(sd.BackendId, data.BackendId) - codec.Put(data) - } - if data, err := codec.DecodePrivate(private); assert.NoError(err) { - assert.Equal(sd.Sid, data.Sid) - assert.Equal(sd.Created, data.Created) - assert.Equal(sd.BackendId, data.BackendId) - codec.Put(data) - } - - if data, err := codec.DecodePublic(api.PublicSessionId(private)); !assert.Error(err) { - assert.Fail("should have failed", "received %+v", data) - codec.Put(data) - } - if data, err := codec.DecodePrivate(api.PrivateSessionId(public)); !assert.Error(err) { - assert.Fail("should have failed", "received %+v", data) - codec.Put(data) - } -} diff --git a/server/session_test.go b/session_test.go similarity index 73% rename from server/session_test.go rename to session_test.go index 5cc677e..b157c80 100644 --- a/server/session_test.go +++ b/session_test.go @@ -19,28 +19,20 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "testing" "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" ) -func assertSessionHasPermission(t *testing.T, session Session, permission api.Permission) { +func assertSessionHasPermission(t *testing.T, session Session, permission Permission) { t.Helper() assert.True(t, session.HasPermission(permission), "Session %s doesn't have permission %s", session.PublicId(), permission) - if cs, ok := session.(*ClientSession); ok { - assert.True(t, cs.HasAnyPermission(permission), "Session %s doesn't have permission %s", session.PublicId(), permission) - } } -func assertSessionHasNotPermission(t *testing.T, session Session, permission api.Permission) { +func assertSessionHasNotPermission(t *testing.T, session Session, permission Permission) { t.Helper() assert.False(t, session.HasPermission(permission), "Session %s has permission %s but shouldn't", session.PublicId(), permission) - if cs, ok := session.(*ClientSession); ok { - assert.False(t, cs.HasAnyPermission(permission), "Session %s has permission %s but shouldn't", session.PublicId(), permission) - } } diff --git a/sessionid_codec.go b/sessionid_codec.go new file mode 100644 index 0000000..81de6a8 --- /dev/null +++ b/sessionid_codec.go @@ -0,0 +1,121 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "encoding/base64" + "fmt" + + "github.com/gorilla/securecookie" + "google.golang.org/protobuf/proto" +) + +type protoSerializer struct { +} + +func (s *protoSerializer) Serialize(src interface{}) ([]byte, error) { + msg, ok := src.(proto.Message) + if !ok { + return nil, fmt.Errorf("can't serialize type %T", src) + } + return proto.Marshal(msg) +} + +func (s *protoSerializer) Deserialize(src []byte, dst interface{}) error { + msg, ok := dst.(proto.Message) + if !ok { + return fmt.Errorf("can't deserialize type %T", src) + } + return proto.Unmarshal(src, msg) +} + +const ( + privateSessionName = "private-session" + publicSessionName = "public-session" +) + +type SessionIdCodec struct { + cookie *securecookie.SecureCookie +} + +func NewSessionIdCodec(hashKey []byte, blockKey []byte) *SessionIdCodec { + cookie := securecookie.New(hashKey, blockKey). + MaxAge(0). + SetSerializer(&protoSerializer{}) + return &SessionIdCodec{ + cookie: cookie, + } +} + +func (c *SessionIdCodec) EncodePrivate(sessionData *SessionIdData) (string, error) { + return c.cookie.Encode(privateSessionName, sessionData) +} + +func reverseSessionId(s string) (string, error) { + // Note that we are assuming base64 encoded strings here. + decoded, err := base64.URLEncoding.DecodeString(s) + if err != nil { + return "", err + } + + for i, j := 0, len(decoded)-1; i < j; i, j = i+1, j-1 { + decoded[i], decoded[j] = decoded[j], decoded[i] + } + return base64.URLEncoding.EncodeToString(decoded), nil +} + +func (c *SessionIdCodec) EncodePublic(sessionData *SessionIdData) (string, error) { + encoded, err := c.cookie.Encode(publicSessionName, sessionData) + if err != nil { + return "", err + } + + // We are reversing the public session ids because clients compare them + // to decide who calls whom. The prefix of the session id is increasing + // (a timestamp) but the suffix the (random) hash. + // By reversing we move the hash to the front, making the comparison of + // session ids "random". + return reverseSessionId(encoded) +} + +func (c *SessionIdCodec) DecodePrivate(encodedData string) (*SessionIdData, error) { + var data SessionIdData + if err := c.cookie.Decode(privateSessionName, encodedData, &data); err != nil { + return nil, err + } + + return &data, nil +} + +func (c *SessionIdCodec) DecodePublic(encodedData string) (*SessionIdData, error) { + encodedData, err := reverseSessionId(encodedData) + if err != nil { + return nil, err + } + + var data SessionIdData + if err := c.cookie.Decode(publicSessionName, encodedData, &data); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/sessionid_codec_test.go b/sessionid_codec_test.go new file mode 100644 index 0000000..6961458 --- /dev/null +++ b/sessionid_codec_test.go @@ -0,0 +1,79 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestReverseSessionId(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + a := base64.URLEncoding.EncodeToString([]byte("12345")) + ar, err := reverseSessionId(a) + require.NoError(err) + require.NotEqual(a, ar) + b := base64.URLEncoding.EncodeToString([]byte("54321")) + br, err := reverseSessionId(b) + require.NoError(err) + require.NotEqual(b, br) + assert.Equal(b, ar) + assert.Equal(a, br) + + // Invalid base64. + if s, err := reverseSessionId("hello world!"); !assert.Error(err) { + assert.Fail("should have failed but got %s", s) + } + // Invalid base64 length. + if s, err := reverseSessionId("123"); !assert.Error(err) { + assert.Fail("should have failed but got %s", s) + } +} + +func TestPublicPrivate(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + sd := &SessionIdData{ + Sid: 1, + Created: timestamppb.Now(), + BackendId: "foo", + } + + codec := NewSessionIdCodec([]byte("0123456789012345"), []byte("0123456789012345")) + private, err := codec.EncodePrivate(sd) + require.NoError(err) + public, err := codec.EncodePublic(sd) + require.NoError(err) + assert.NotEqual(private, public) + + if data, err := codec.DecodePublic(private); !assert.Error(err) { + assert.Fail("should have failed but got %+v", data) + } + if data, err := codec.DecodePrivate(public); !assert.Error(err) { + assert.Fail("should have failed but got %+v", data) + } +} diff --git a/sfu/common.go b/sfu/common.go deleted file mode 100644 index 00aed84..0000000 --- a/sfu/common.go +++ /dev/null @@ -1,260 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package sfu - -import ( - "context" - "errors" - "fmt" - "net" - "time" - - "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -const ( - TypeJanus = "janus" - TypeProxy = "proxy" - - TypeDefault = TypeJanus -) - -var ( - ErrNotConnected = errors.New("not connected") -) - -type MediaType int - -const ( - MediaTypeAudio MediaType = 1 << 0 - MediaTypeVideo MediaType = 1 << 1 - MediaTypeScreen MediaType = 1 << 2 -) - -type Listener interface { - PublicId() api.PublicSessionId - - OnUpdateOffer(client Client, offer api.StringMap) - - OnIceCandidate(client Client, candidate any) - OnIceCompleted(client Client) - - SubscriberSidUpdated(subscriber Subscriber) - - PublisherClosed(publisher Publisher) - SubscriberClosed(subscriber Subscriber) -} - -type Initiator interface { - Country() geoip.Country -} - -type Settings interface { - MaxStreamBitrate() api.Bandwidth - MaxScreenBitrate() api.Bandwidth - Timeout() time.Duration - - Reload(config *goconf.ConfigFile) -} - -type NewPublisherSettings struct { - Bitrate api.Bandwidth `json:"bitrate,omitempty"` - MediaTypes MediaType `json:"mediatypes,omitempty"` - - AudioCodec string `json:"audiocodec,omitempty"` - VideoCodec string `json:"videocodec,omitempty"` - VP9Profile string `json:"vp9_profile,omitempty"` - H264Profile string `json:"h264_profile,omitempty"` -} - -type SFU interface { - Start(ctx context.Context) error - Stop() - Reload(config *goconf.ConfigFile) - - SetOnConnected(func()) - SetOnDisconnected(func()) - - GetStats() any - GetServerInfoSfu() *talk.BackendServerInfoSfu - GetBandwidthLimits() (api.Bandwidth, api.Bandwidth) - - NewPublisher(ctx context.Context, listener Listener, id api.PublicSessionId, sid string, streamType StreamType, settings NewPublisherSettings, initiator Initiator) (Publisher, error) - NewSubscriber(ctx context.Context, listener Listener, publisher api.PublicSessionId, streamType StreamType, initiator Initiator) (Subscriber, error) -} - -// PublisherStream contains the available properties when creating a -// remote publisher in Janus. -type PublisherStream struct { - Mid string `json:"mid"` - Mindex int `json:"mindex"` - Type string `json:"type"` - - Description string `json:"description,omitempty"` - Disabled bool `json:"disabled,omitempty"` - - // For types "audio" and "video" - Codec string `json:"codec,omitempty"` - - // For type "audio" - Stereo bool `json:"stereo,omitempty"` - Fec bool `json:"fec,omitempty"` - Dtx bool `json:"dtx,omitempty"` - - // For type "video" - Simulcast bool `json:"simulcast,omitempty"` - Svc bool `json:"svc,omitempty"` - - ProfileH264 string `json:"h264_profile,omitempty"` - ProfileVP9 string `json:"vp9_profile,omitempty"` - - ExtIdVideoOrientation int `json:"videoorient_ext_id,omitempty"` - ExtIdPlayoutDelay int `json:"playoutdelay_ext_id,omitempty"` -} - -type RemotePublisherController interface { - PublisherId() api.PublicSessionId - - StartPublishing(ctx context.Context, publisher RemotePublisherProperties) error - StopPublishing(ctx context.Context, publisher RemotePublisherProperties) error - GetStreams(ctx context.Context) ([]PublisherStream, error) -} - -type RemoteSfu interface { - NewRemotePublisher(ctx context.Context, listener Listener, controller RemotePublisherController, streamType StreamType) (RemotePublisher, error) - NewRemoteSubscriber(ctx context.Context, listener Listener, publisher RemotePublisher) (RemoteSubscriber, error) -} - -type WithToken interface { - CreateToken(subject string) (string, error) -} - -type StreamType string - -const ( - StreamTypeAudio StreamType = "audio" - StreamTypeVideo StreamType = "video" - StreamTypeScreen StreamType = "screen" -) - -func IsValidStreamType(s string) bool { - switch s { - case string(StreamTypeAudio): - fallthrough - case string(StreamTypeVideo): - fallthrough - case string(StreamTypeScreen): - return true - default: - return false - } -} - -type StreamId string - -func GetStreamId(publisherId api.PublicSessionId, streamType StreamType) StreamId { - return StreamId(fmt.Sprintf("%s|%s", publisherId, streamType)) -} - -type ClientBandwidthInfo struct { - // Sent is the outgoing bandwidth. - Sent api.Bandwidth - // Received is the incoming bandwidth. - Received api.Bandwidth -} - -type Client interface { - Id() string - Sid() string - StreamType() StreamType - // MaxBitrate is the maximum allowed bitrate. - MaxBitrate() api.Bandwidth - - Close(ctx context.Context) - - SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) -} - -type ClientWithBandwidth interface { - Client - - Bandwidth() *ClientBandwidthInfo -} - -type Publisher interface { - Client - - PublisherId() api.PublicSessionId - - HasMedia(MediaType) bool - SetMedia(MediaType) -} - -type PublisherWithStreams interface { - Publisher - - GetStreams(ctx context.Context) ([]PublisherStream, error) -} - -type RemoteAwarePublisher interface { - Publisher - - PublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error - UnpublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error -} - -type PublisherWithConnectionUrlAndIP interface { - Publisher - - GetConnectionURL() (string, net.IP) -} - -type Subscriber interface { - Client - - Publisher() api.PublicSessionId -} - -type SubscriberWithConnectionUrlAndIP interface { - Subscriber - - GetConnectionURL() (string, net.IP) -} - -type RemotePublisherProperties interface { - Port() int - RtcpPort() int -} - -type RemotePublisher interface { - Client - - RemotePublisherProperties -} - -type RemoteSubscriber interface { - Subscriber -} diff --git a/sfu/internal/settings.go b/sfu/internal/settings.go deleted file mode 100644 index 1dc2ebd..0000000 --- a/sfu/internal/settings.go +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "sync/atomic" - "time" - - "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" -) - -var ( - defaultMaxStreamBitrate = api.BandwidthFromMegabits(1) - defaultMaxScreenBitrate = api.BandwidthFromMegabits(2) -) - -type CommonSettings struct { - Logger log.Logger - - maxStreamBitrate api.AtomicBandwidth - maxScreenBitrate api.AtomicBandwidth - - timeout atomic.Int64 -} - -func (s *CommonSettings) MaxStreamBitrate() api.Bandwidth { - return s.maxStreamBitrate.Load() -} - -func (s *CommonSettings) MaxScreenBitrate() api.Bandwidth { - return s.maxScreenBitrate.Load() -} - -func (s *CommonSettings) Timeout() time.Duration { - return time.Duration(s.timeout.Load()) -} - -func (s *CommonSettings) SetTimeout(timeout time.Duration) { - s.timeout.Store(timeout.Nanoseconds()) -} - -func (s *CommonSettings) Load(config *goconf.ConfigFile) error { - maxStreamBitrateValue, _ := config.GetInt("mcu", "maxstreambitrate") - if maxStreamBitrateValue <= 0 { - maxStreamBitrateValue = int(defaultMaxStreamBitrate.Bits()) - } - maxStreamBitrate := api.BandwidthFromBits(uint64(maxStreamBitrateValue)) - s.Logger.Printf("Maximum bandwidth %s per publishing stream", maxStreamBitrate) - s.maxStreamBitrate.Store(maxStreamBitrate) - - maxScreenBitrateValue, _ := config.GetInt("mcu", "maxscreenbitrate") - if maxScreenBitrateValue <= 0 { - maxScreenBitrateValue = int(defaultMaxScreenBitrate.Bits()) - } - maxScreenBitrate := api.BandwidthFromBits(uint64(maxScreenBitrateValue)) - s.Logger.Printf("Maximum bandwidth %s per screensharing stream", maxScreenBitrate) - s.maxScreenBitrate.Store(maxScreenBitrate) - return nil -} diff --git a/sfu/internal/stats_prometheus.go b/sfu/internal/stats_prometheus.go deleted file mode 100644 index a79448a..0000000 --- a/sfu/internal/stats_prometheus.go +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package internal - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" -) - -var ( - StatsPublishersCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "publishers", - Help: "The current number of publishers", - }, []string{"type"}) - StatsPublishersTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "publishers_total", - Help: "The total number of created publishers", - }, []string{"type"}) - StatsSubscribersCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "subscribers", - Help: "The current number of subscribers", - }, []string{"type"}) - StatsSubscribersTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "subscribers_total", - Help: "The total number of created subscribers", - }, []string{"type"}) - StatsWaitingForPublisherTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "nopublisher_total", - Help: "The total number of subscribe requests where no publisher exists", - }, []string{"type"}) - StatsMcuMessagesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "messages_total", - Help: "The total number of MCU messages", - }, []string{"type"}) - - commonMcuStats = []prometheus.Collector{ - StatsPublishersCurrent, - StatsPublishersTotal, - StatsSubscribersCurrent, - StatsSubscribersTotal, - StatsWaitingForPublisherTotal, - StatsMcuMessagesTotal, - } -) - -func RegisterCommonStats() { - metrics.RegisterAll(commonMcuStats...) -} - -func UnregisterCommonStats() { - metrics.UnregisterAll(commonMcuStats...) -} diff --git a/sfu/janus/api.go b/sfu/janus/api.go deleted file mode 100644 index b28c7b6..0000000 --- a/sfu/janus/api.go +++ /dev/null @@ -1,514 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "encoding/json" - "fmt" -) - -const ( - janusEventTypeSession = 1 - - janusEventTypeHandle = 2 - - janusEventTypeExternal = 4 - - janusEventTypeJSEP = 8 - - janusEventTypeWebRTC = 16 - janusEventSubTypeWebRTCICE = 1 - janusEventSubTypeWebRTCLocalCandidate = 2 - janusEventSubTypeWebRTCRemoteCandidate = 3 - janusEventSubTypeWebRTCSelectedPair = 4 - janusEventSubTypeWebRTCDTLS = 5 - janusEventSubTypeWebRTCPeerConnection = 6 - - janusEventTypeMedia = 32 - janusEventSubTypeMediaState = 1 - janusEventSubTypeMediaSlowLink = 2 - janusEventSubTypeMediaStats = 3 - - janusEventTypePlugin = 64 - - janusEventTypeTransport = 128 - - janusEventTypeCore = 256 - janusEventSubTypeCoreStatusStartup = 1 - janusEventSubTypeCoreStatusShutdown = 2 -) - -func unmarshalEvent[T any](data json.RawMessage) (*T, error) { - var e T - if err := json.Unmarshal(data, &e); err != nil { - return nil, err - } - - return &e, nil -} - -func marshalEvent[T any](e T) string { - data, err := json.Marshal(e) - if err != nil { - return fmt.Sprintf("Could not serialize %#v: %s", e, err) - } - - return string(data) -} - -type janusEvent struct { - Emitter string `json:"emitter"` - Type int `json:"type"` - SubType int `json:"subtype,omitempty"` - Timestamp uint64 `json:"timestamp"` - SessionId uint64 `json:"session_id,omitempty"` - HandleId uint64 `json:"handle_id,omitempty"` - OpaqueId uint64 `json:"opaque_id,omitempty"` - Event json.RawMessage `json:"event"` -} - -func (e janusEvent) String() string { - return marshalEvent(e) -} - -func (e janusEvent) Decode() (any, error) { - switch e.Type { - case janusEventTypeSession: - return unmarshalEvent[janusEventSession](e.Event) - case janusEventTypeHandle: - return unmarshalEvent[janusEventHandle](e.Event) - case janusEventTypeExternal: - return unmarshalEvent[janusEventExternal](e.Event) - case janusEventTypeJSEP: - return unmarshalEvent[janusEventJSEP](e.Event) - case janusEventTypeWebRTC: - switch e.SubType { - case janusEventSubTypeWebRTCICE: - return unmarshalEvent[janusEventWebRTCICE](e.Event) - case janusEventSubTypeWebRTCLocalCandidate: - return unmarshalEvent[janusEventWebRTCLocalCandidate](e.Event) - case janusEventSubTypeWebRTCRemoteCandidate: - return unmarshalEvent[janusEventWebRTCRemoteCandidate](e.Event) - case janusEventSubTypeWebRTCSelectedPair: - return unmarshalEvent[janusEventWebRTCSelectedPair](e.Event) - case janusEventSubTypeWebRTCDTLS: - return unmarshalEvent[janusEventWebRTCDTLS](e.Event) - case janusEventSubTypeWebRTCPeerConnection: - return unmarshalEvent[janusEventWebRTCPeerConnection](e.Event) - } - case janusEventTypeMedia: - switch e.SubType { - case janusEventSubTypeMediaState: - return unmarshalEvent[janusEventMediaState](e.Event) - case janusEventSubTypeMediaSlowLink: - return unmarshalEvent[janusEventMediaSlowLink](e.Event) - case janusEventSubTypeMediaStats: - return unmarshalEvent[janusEventMediaStats](e.Event) - } - case janusEventTypePlugin: - return unmarshalEvent[janusEventPlugin](e.Event) - case janusEventTypeTransport: - return unmarshalEvent[janusEventTransport](e.Event) - case janusEventTypeCore: - switch e.SubType { - case janusEventSubTypeCoreStatusStartup: - event, err := unmarshalEvent[janusEventCoreStartup](e.Event) - if err != nil { - return nil, err - } - - switch event.Status { - case "started": - return unmarshalEvent[janusEventStatusStartupInfo](event.Info) - case "update": - return unmarshalEvent[janusEventStatusUpdateInfo](event.Info) - } - - return event, nil - case janusEventSubTypeCoreStatusShutdown: - return unmarshalEvent[janusEventCoreShutdown](e.Event) - } - } - - return nil, fmt.Errorf("unsupported event type %d", e.Type) -} - -type janusEventSessionTransport struct { - Transport string `json:"transport"` - ID string `json:"id"` -} - -// type=1 -type janusEventSession struct { - Name string `json:"name"` // "created", "destroyed", "timeout" - - Transport *janusEventSessionTransport `json:"transport,omitempty"` -} - -func (e janusEventSession) String() string { - return marshalEvent(e) -} - -// type=2 -type janusEventHandle struct { - Name string `json:"name"` // "attached", "detached" - Plugin string `json:"plugin"` - Token string `json:"token,omitempty"` - // Deprecated - OpaqueId string `json:"opaque_id,omitempty"` -} - -func (e janusEventHandle) String() string { - return marshalEvent(e) -} - -// type=4 -type janusEventExternal struct { - Schema string `json:"schema"` - Data json.RawMessage `json:"data"` -} - -func (e janusEventExternal) String() string { - return marshalEvent(e) -} - -// type=8 -type janusEventJSEP struct { - Owner string `json:"owner"` - Jsep struct { - Type string `json:"type"` - SDP string `json:"sdp"` - } `json:"jsep"` -} - -func (e janusEventJSEP) String() string { - return marshalEvent(e) -} - -// type=16, subtype=1 -type janusEventWebRTCICE struct { - ICE string `json:"ice"` // "gathering", "connecting", "connected", "ready" - StreamID int `json:"stream_id"` - ComponentID int `json:"component_id"` -} - -func (e janusEventWebRTCICE) String() string { - return marshalEvent(e) -} - -// type=16, subtype=2 -type janusEventWebRTCLocalCandidate struct { - LocalCandidate string `json:"local-candidate"` - StreamID int `json:"stream_id"` - ComponentID int `json:"component_id"` -} - -func (e janusEventWebRTCLocalCandidate) String() string { - return marshalEvent(e) -} - -// type=16, subtype=3 -type janusEventWebRTCRemoteCandidate struct { - RemoteCandidate string `json:"remote-candidate"` - StreamID int `json:"stream_id"` - ComponentID int `json:"component_id"` -} - -func (e janusEventWebRTCRemoteCandidate) String() string { - return marshalEvent(e) -} - -type janusEventCandidate struct { - Address string `json:"address"` - Port int `json:"port"` - Type string `json:"type"` - Transport string `json:"transport"` - Family int `json:"family"` -} - -func (e janusEventCandidate) String() string { - return marshalEvent(e) -} - -type janusEventCandidates struct { - Local janusEventCandidate `json:"local"` - Remote janusEventCandidate `json:"remote"` -} - -func (e janusEventCandidates) String() string { - return marshalEvent(e) -} - -// type=16, subtype=4 -type janusEventWebRTCSelectedPair struct { - StreamID int `json:"stream_id"` - ComponentID int `json:"component_id"` - - SelectedPair string `json:"selected-pair"` - Candidates janusEventCandidates `json:"candidates"` -} - -func (e janusEventWebRTCSelectedPair) String() string { - return marshalEvent(e) -} - -// type=16, subtype=5 -type janusEventWebRTCDTLS struct { - DTLS string `json:"dtls"` // "trying", "connected" - - StreamID int `json:"stream_id"` - ComponentID int `json:"component_id"` - - Retransmissions int `json:"retransmissions"` -} - -func (e janusEventWebRTCDTLS) String() string { - return marshalEvent(e) -} - -// type=16, subtype=6 -type janusEventWebRTCPeerConnection struct { - Connection string `json:"connection"` // "webrtcup", "hangup" - Reason string `json:"reason,omitempty"` // Only if "connection" == "hangup" -} - -func (e janusEventWebRTCPeerConnection) String() string { - return marshalEvent(e) -} - -// type=32, subtype=1 -type janusEventMediaState struct { - Media string `json:"media"` // "audio", "video" - MID string `json:"mid"` - SubStream *int `json:"substream,omitempty"` - Receiving bool `json:"receiving"` - Seconds int `json:"seconds"` -} - -func (e janusEventMediaState) String() string { - return marshalEvent(e) -} - -// type=32, subtype=2 -type janusEventMediaSlowLink struct { - Media string `json:"media"` // "audio", "video" - MID string `json:"mid"` - SlowLink string `json:"slow_link"` // "uplink", "downlink" - LostLastSec int `json:"lost_lastsec"` -} - -func (e janusEventMediaSlowLink) String() string { - return marshalEvent(e) -} - -type janusMediaStatsRTTValues struct { - NTP uint32 `json:"ntp"` - LSR uint32 `json:"lsr"` - DLSR uint32 `json:"dlsr"` -} - -func (e janusMediaStatsRTTValues) String() string { - return marshalEvent(e) -} - -// type=32, subtype=3 -type janusEventMediaStats struct { - MID string `json:"mid"` - MIndex int `json:"mindex"` - Media string `json:"media"` // "audio", "video", "video-sim1", "video-sim2" - - // Audio / video only - Codec string `json:"codec,omitempty"` - Base uint32 `json:"base"` - Lost int32 `json:"lost"` - LostByRemote int32 `json:"lost-by-remote"` - JitterLocal uint32 `json:"jitter-local"` - JitterRemote uint32 `json:"jitter-remote"` - InLinkQuality uint32 `json:"in-link-quality"` - InMediaLinkQuality uint32 `json:"in-media-link-quality"` - OutLinkQuality uint32 `json:"out-link-quality"` - OutMediaLinkQuality uint32 `json:"out-media-link-quality"` - BytesReceivedLastSec uint32 `json:"bytes-received-lastsec"` - BytesSentLastSec uint32 `json:"bytes-sent-lastsec"` - NacksReceived uint32 `json:"nacks-received"` - NacksSent uint32 `json:"nacks-sent"` - RetransmissionsReceived uint32 `json:"retransmissions-received"` - - // Only for audio / video on layer 0 - RTT uint32 `json:"rtt,omitempty"` - // Only for audio / video on layer 0 if RTCP is active - RTTValues *janusMediaStatsRTTValues `json:"rtt-values,omitempty"` - - // For all media on all layers - PacketsReceived uint32 `json:"packets-received"` - PacketsSent uint32 `json:"packets-sent"` - BytesReceived uint64 `json:"bytes-received"` - BytesSent uint64 `json:"bytes-sent"` - - // For layer 0 if REMB is enabled - REMBBitrate uint32 `json:"remb-bitrate"` -} - -func (e janusEventMediaStats) String() string { - return marshalEvent(e) -} - -// type=64 -type janusEventPlugin struct { - Plugin string `json:"plugin"` - Data json.RawMessage `json:"data"` -} - -func (e janusEventPlugin) String() string { - return marshalEvent(e) -} - -type janusEventTransportWebsocket struct { - Event string `json:"event"` - AdminApi bool `json:"admin_api,omitempty"` - IP string `json:"ip,omitempty"` -} - -// type=128 -type janusEventTransport struct { - Transport string `json:"transport"` - Id string `json:"id"` - Data janusEventTransportWebsocket `json:"data"` -} - -func (e janusEventTransport) String() string { - return marshalEvent(e) -} - -type janusEventDependenciesInfo struct { - Glib2 string `json:"glib2"` - Jansson string `json:"jansson"` - Libnice string `json:"libnice"` - Libsrtp string `json:"libsrtp"` - Libcurl string `json:"libcurl,omitempty"` - Crypto string `json:"crypto"` -} - -func (e janusEventDependenciesInfo) String() string { - return marshalEvent(e) -} - -type janusEventPluginInfo struct { - Name string `json:"name"` - Author string `json:"author"` - Description string `json:"description"` - VersionString string `json:"version_string"` - Version int `json:"version"` -} - -func (e janusEventPluginInfo) String() string { - return marshalEvent(e) -} - -// type=256, subtype=1, status="startup" -type janusEventStatusStartupInfo struct { - Janus string `json:"janus"` - Version int `json:"version"` - VersionString string `json:"version_string"` - Author string `json:"author"` - CommitHash string `json:"commit-hash"` - CompileTime string `json:"compile-time"` - LogToStdout bool `json:"log-to-stdout"` - LogToFile bool `json:"log-to-file"` - LogPath string `json:"log-path,omitempty"` - DataChannels bool `json:"data_channels"` - AcceptingNewSessions bool `json:"accepting-new-sessions"` - SessionTimeout int `json:"session-timeout"` - ReclaimSessionTimeout int `json:"reclaim-session-timeout"` - CandidatesTimeout int `json:"candidates-timeout"` - ServerName string `json:"server-name"` - LocalIP string `json:"local-ip"` - PublicIP string `json:"public-ip,omitempty"` - PublicIPs []string `json:"public-ips,omitempty"` - IPv6 bool `json:"ipv6"` - IPv6LinkLocal bool `json:"ipv6-link-local,omitempty"` - ICELite bool `json:"ice-lite"` - ICETCP bool `json:"ice-tcp"` - ICENomination string `json:"ice-nomination,omitempty"` - ICEConsentFreshness bool `json:"ice-consent-freshness"` - ICEKeepaliveConncheck bool `json:"ice-keepalive-conncheck"` - HangupOnFailed bool `json:"hangup-on-failed"` - FullTrickle bool `json:"full-trickle"` - MDNSEnabled bool `json:"mdns-enabled"` - MinNACKQueue int `json:"min-nack-queue"` - NACKOptimizations bool `json:"nack-optimizations"` - TWCCPeriod int `json:"twcc-period"` - DSCP int `json:"dscp,omitempty"` - DTLSMCU int `json:"dtls-mcu"` - STUNServer string `json:"stun-server,omitempty"` - TURNServer string `json:"turn-server,omitempty"` - AllowForceRelay bool `json:"allow-force-relay,omitempty"` - StaticEventLoops int `json:"static-event-loops"` - LoopIndication bool `json:"loop-indication,omitempty"` - APISecret bool `json:"api_secret"` - AuthToken bool `json:"auth_token"` - EventHandlers bool `json:"event_handlers"` - OpaqueIdInAPI bool `json:"opaqueid_in_api"` - WebRTCEncryption bool `json:"webrtc_encryption"` - - Dependencies *janusEventDependenciesInfo `json:"dependencies,omitempty"` - Transports map[string]janusEventPluginInfo `json:"transports,omitempty"` - Events map[string]janusEventPluginInfo `json:"events,omitempty"` - Loggers map[string]janusEventPluginInfo `json:"loggers,omitempty"` - Plugins map[string]janusEventPluginInfo `json:"plugins,omitempty"` -} - -func (e janusEventStatusStartupInfo) String() string { - return marshalEvent(e) -} - -// type=256, subtype=1, status="update" -type janusEventStatusUpdateInfo struct { - Sessions int `json:"sessions"` - Handles int `json:"handles"` - PeerConnections int `json:"peerconnections"` - StatsPeriod int `json:"stats-period"` -} - -func (e janusEventStatusUpdateInfo) String() string { - return marshalEvent(e) -} - -// type=256, subtype=1 -type janusEventCoreStartup struct { - Status string `json:"status"` - Info json.RawMessage `json:"info"` -} - -func (e janusEventCoreStartup) String() string { - return marshalEvent(e) -} - -// type=256, subtype=2 -type janusEventCoreShutdown struct { - Status string `json:"status"` - Signum int `json:"signum"` -} - -func (e janusEventCoreShutdown) String() string { - return marshalEvent(e) -} diff --git a/sfu/janus/api_easyjson.go b/sfu/janus/api_easyjson.go deleted file mode 100644 index ed78b1a..0000000 --- a/sfu/janus/api_easyjson.go +++ /dev/null @@ -1,3467 +0,0 @@ -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. - -package janus - -import ( - json "encoding/json" - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" -) - -// suppress unused package warning -var ( - _ *json.RawMessage - _ *jlexer.Lexer - _ *jwriter.Writer - _ easyjson.Marshaler -) - -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus(in *jlexer.Lexer, out *janusMediaStatsRTTValues) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "ntp": - if in.IsNull() { - in.Skip() - } else { - out.NTP = uint32(in.Uint32()) - } - case "lsr": - if in.IsNull() { - in.Skip() - } else { - out.LSR = uint32(in.Uint32()) - } - case "dlsr": - if in.IsNull() { - in.Skip() - } else { - out.DLSR = uint32(in.Uint32()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus(out *jwriter.Writer, in janusMediaStatsRTTValues) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"ntp\":" - out.RawString(prefix[1:]) - out.Uint32(uint32(in.NTP)) - } - { - const prefix string = ",\"lsr\":" - out.RawString(prefix) - out.Uint32(uint32(in.LSR)) - } - { - const prefix string = ",\"dlsr\":" - out.RawString(prefix) - out.Uint32(uint32(in.DLSR)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusMediaStatsRTTValues) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusMediaStatsRTTValues) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusMediaStatsRTTValues) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusMediaStatsRTTValues) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus1(in *jlexer.Lexer, out *janusEventWebRTCSelectedPair) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "stream_id": - if in.IsNull() { - in.Skip() - } else { - out.StreamID = int(in.Int()) - } - case "component_id": - if in.IsNull() { - in.Skip() - } else { - out.ComponentID = int(in.Int()) - } - case "selected-pair": - if in.IsNull() { - in.Skip() - } else { - out.SelectedPair = string(in.String()) - } - case "candidates": - if in.IsNull() { - in.Skip() - } else { - (out.Candidates).UnmarshalEasyJSON(in) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus1(out *jwriter.Writer, in janusEventWebRTCSelectedPair) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"stream_id\":" - out.RawString(prefix[1:]) - out.Int(int(in.StreamID)) - } - { - const prefix string = ",\"component_id\":" - out.RawString(prefix) - out.Int(int(in.ComponentID)) - } - { - const prefix string = ",\"selected-pair\":" - out.RawString(prefix) - out.String(string(in.SelectedPair)) - } - { - const prefix string = ",\"candidates\":" - out.RawString(prefix) - (in.Candidates).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventWebRTCSelectedPair) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus1(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventWebRTCSelectedPair) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus1(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventWebRTCSelectedPair) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus1(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventWebRTCSelectedPair) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus1(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus2(in *jlexer.Lexer, out *janusEventWebRTCRemoteCandidate) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "remote-candidate": - if in.IsNull() { - in.Skip() - } else { - out.RemoteCandidate = string(in.String()) - } - case "stream_id": - if in.IsNull() { - in.Skip() - } else { - out.StreamID = int(in.Int()) - } - case "component_id": - if in.IsNull() { - in.Skip() - } else { - out.ComponentID = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus2(out *jwriter.Writer, in janusEventWebRTCRemoteCandidate) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"remote-candidate\":" - out.RawString(prefix[1:]) - out.String(string(in.RemoteCandidate)) - } - { - const prefix string = ",\"stream_id\":" - out.RawString(prefix) - out.Int(int(in.StreamID)) - } - { - const prefix string = ",\"component_id\":" - out.RawString(prefix) - out.Int(int(in.ComponentID)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventWebRTCRemoteCandidate) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus2(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventWebRTCRemoteCandidate) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus2(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventWebRTCRemoteCandidate) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus2(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventWebRTCRemoteCandidate) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus2(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus3(in *jlexer.Lexer, out *janusEventWebRTCPeerConnection) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "connection": - if in.IsNull() { - in.Skip() - } else { - out.Connection = string(in.String()) - } - case "reason": - if in.IsNull() { - in.Skip() - } else { - out.Reason = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus3(out *jwriter.Writer, in janusEventWebRTCPeerConnection) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"connection\":" - out.RawString(prefix[1:]) - out.String(string(in.Connection)) - } - if in.Reason != "" { - const prefix string = ",\"reason\":" - out.RawString(prefix) - out.String(string(in.Reason)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventWebRTCPeerConnection) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus3(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventWebRTCPeerConnection) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus3(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventWebRTCPeerConnection) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus3(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventWebRTCPeerConnection) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus3(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus4(in *jlexer.Lexer, out *janusEventWebRTCLocalCandidate) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "local-candidate": - if in.IsNull() { - in.Skip() - } else { - out.LocalCandidate = string(in.String()) - } - case "stream_id": - if in.IsNull() { - in.Skip() - } else { - out.StreamID = int(in.Int()) - } - case "component_id": - if in.IsNull() { - in.Skip() - } else { - out.ComponentID = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus4(out *jwriter.Writer, in janusEventWebRTCLocalCandidate) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"local-candidate\":" - out.RawString(prefix[1:]) - out.String(string(in.LocalCandidate)) - } - { - const prefix string = ",\"stream_id\":" - out.RawString(prefix) - out.Int(int(in.StreamID)) - } - { - const prefix string = ",\"component_id\":" - out.RawString(prefix) - out.Int(int(in.ComponentID)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventWebRTCLocalCandidate) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus4(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventWebRTCLocalCandidate) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus4(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventWebRTCLocalCandidate) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus4(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventWebRTCLocalCandidate) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus4(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus5(in *jlexer.Lexer, out *janusEventWebRTCICE) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "ice": - if in.IsNull() { - in.Skip() - } else { - out.ICE = string(in.String()) - } - case "stream_id": - if in.IsNull() { - in.Skip() - } else { - out.StreamID = int(in.Int()) - } - case "component_id": - if in.IsNull() { - in.Skip() - } else { - out.ComponentID = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus5(out *jwriter.Writer, in janusEventWebRTCICE) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"ice\":" - out.RawString(prefix[1:]) - out.String(string(in.ICE)) - } - { - const prefix string = ",\"stream_id\":" - out.RawString(prefix) - out.Int(int(in.StreamID)) - } - { - const prefix string = ",\"component_id\":" - out.RawString(prefix) - out.Int(int(in.ComponentID)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventWebRTCICE) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus5(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventWebRTCICE) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus5(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventWebRTCICE) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus5(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventWebRTCICE) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus5(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus6(in *jlexer.Lexer, out *janusEventWebRTCDTLS) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "dtls": - if in.IsNull() { - in.Skip() - } else { - out.DTLS = string(in.String()) - } - case "stream_id": - if in.IsNull() { - in.Skip() - } else { - out.StreamID = int(in.Int()) - } - case "component_id": - if in.IsNull() { - in.Skip() - } else { - out.ComponentID = int(in.Int()) - } - case "retransmissions": - if in.IsNull() { - in.Skip() - } else { - out.Retransmissions = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus6(out *jwriter.Writer, in janusEventWebRTCDTLS) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"dtls\":" - out.RawString(prefix[1:]) - out.String(string(in.DTLS)) - } - { - const prefix string = ",\"stream_id\":" - out.RawString(prefix) - out.Int(int(in.StreamID)) - } - { - const prefix string = ",\"component_id\":" - out.RawString(prefix) - out.Int(int(in.ComponentID)) - } - { - const prefix string = ",\"retransmissions\":" - out.RawString(prefix) - out.Int(int(in.Retransmissions)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventWebRTCDTLS) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus6(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventWebRTCDTLS) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus6(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventWebRTCDTLS) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus6(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventWebRTCDTLS) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus6(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus7(in *jlexer.Lexer, out *janusEventTransportWebsocket) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "event": - if in.IsNull() { - in.Skip() - } else { - out.Event = string(in.String()) - } - case "admin_api": - if in.IsNull() { - in.Skip() - } else { - out.AdminApi = bool(in.Bool()) - } - case "ip": - if in.IsNull() { - in.Skip() - } else { - out.IP = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus7(out *jwriter.Writer, in janusEventTransportWebsocket) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"event\":" - out.RawString(prefix[1:]) - out.String(string(in.Event)) - } - if in.AdminApi { - const prefix string = ",\"admin_api\":" - out.RawString(prefix) - out.Bool(bool(in.AdminApi)) - } - if in.IP != "" { - const prefix string = ",\"ip\":" - out.RawString(prefix) - out.String(string(in.IP)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventTransportWebsocket) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus7(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventTransportWebsocket) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus7(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventTransportWebsocket) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus7(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventTransportWebsocket) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus7(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus8(in *jlexer.Lexer, out *janusEventTransport) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "transport": - if in.IsNull() { - in.Skip() - } else { - out.Transport = string(in.String()) - } - case "id": - if in.IsNull() { - in.Skip() - } else { - out.Id = string(in.String()) - } - case "data": - if in.IsNull() { - in.Skip() - } else { - (out.Data).UnmarshalEasyJSON(in) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus8(out *jwriter.Writer, in janusEventTransport) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"transport\":" - out.RawString(prefix[1:]) - out.String(string(in.Transport)) - } - { - const prefix string = ",\"id\":" - out.RawString(prefix) - out.String(string(in.Id)) - } - { - const prefix string = ",\"data\":" - out.RawString(prefix) - (in.Data).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventTransport) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus8(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventTransport) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus8(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventTransport) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus8(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventTransport) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus8(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus9(in *jlexer.Lexer, out *janusEventStatusUpdateInfo) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "sessions": - if in.IsNull() { - in.Skip() - } else { - out.Sessions = int(in.Int()) - } - case "handles": - if in.IsNull() { - in.Skip() - } else { - out.Handles = int(in.Int()) - } - case "peerconnections": - if in.IsNull() { - in.Skip() - } else { - out.PeerConnections = int(in.Int()) - } - case "stats-period": - if in.IsNull() { - in.Skip() - } else { - out.StatsPeriod = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus9(out *jwriter.Writer, in janusEventStatusUpdateInfo) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"sessions\":" - out.RawString(prefix[1:]) - out.Int(int(in.Sessions)) - } - { - const prefix string = ",\"handles\":" - out.RawString(prefix) - out.Int(int(in.Handles)) - } - { - const prefix string = ",\"peerconnections\":" - out.RawString(prefix) - out.Int(int(in.PeerConnections)) - } - { - const prefix string = ",\"stats-period\":" - out.RawString(prefix) - out.Int(int(in.StatsPeriod)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventStatusUpdateInfo) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus9(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventStatusUpdateInfo) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus9(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventStatusUpdateInfo) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus9(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventStatusUpdateInfo) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus9(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus10(in *jlexer.Lexer, out *janusEventStatusStartupInfo) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "janus": - if in.IsNull() { - in.Skip() - } else { - out.Janus = string(in.String()) - } - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = int(in.Int()) - } - case "version_string": - if in.IsNull() { - in.Skip() - } else { - out.VersionString = string(in.String()) - } - case "author": - if in.IsNull() { - in.Skip() - } else { - out.Author = string(in.String()) - } - case "commit-hash": - if in.IsNull() { - in.Skip() - } else { - out.CommitHash = string(in.String()) - } - case "compile-time": - if in.IsNull() { - in.Skip() - } else { - out.CompileTime = string(in.String()) - } - case "log-to-stdout": - if in.IsNull() { - in.Skip() - } else { - out.LogToStdout = bool(in.Bool()) - } - case "log-to-file": - if in.IsNull() { - in.Skip() - } else { - out.LogToFile = bool(in.Bool()) - } - case "log-path": - if in.IsNull() { - in.Skip() - } else { - out.LogPath = string(in.String()) - } - case "data_channels": - if in.IsNull() { - in.Skip() - } else { - out.DataChannels = bool(in.Bool()) - } - case "accepting-new-sessions": - if in.IsNull() { - in.Skip() - } else { - out.AcceptingNewSessions = bool(in.Bool()) - } - case "session-timeout": - if in.IsNull() { - in.Skip() - } else { - out.SessionTimeout = int(in.Int()) - } - case "reclaim-session-timeout": - if in.IsNull() { - in.Skip() - } else { - out.ReclaimSessionTimeout = int(in.Int()) - } - case "candidates-timeout": - if in.IsNull() { - in.Skip() - } else { - out.CandidatesTimeout = int(in.Int()) - } - case "server-name": - if in.IsNull() { - in.Skip() - } else { - out.ServerName = string(in.String()) - } - case "local-ip": - if in.IsNull() { - in.Skip() - } else { - out.LocalIP = string(in.String()) - } - case "public-ip": - if in.IsNull() { - in.Skip() - } else { - out.PublicIP = string(in.String()) - } - case "public-ips": - if in.IsNull() { - in.Skip() - out.PublicIPs = nil - } else { - in.Delim('[') - if out.PublicIPs == nil { - if !in.IsDelim(']') { - out.PublicIPs = make([]string, 0, 4) - } else { - out.PublicIPs = []string{} - } - } else { - out.PublicIPs = (out.PublicIPs)[:0] - } - for !in.IsDelim(']') { - var v1 string - if in.IsNull() { - in.Skip() - } else { - v1 = string(in.String()) - } - out.PublicIPs = append(out.PublicIPs, v1) - in.WantComma() - } - in.Delim(']') - } - case "ipv6": - if in.IsNull() { - in.Skip() - } else { - out.IPv6 = bool(in.Bool()) - } - case "ipv6-link-local": - if in.IsNull() { - in.Skip() - } else { - out.IPv6LinkLocal = bool(in.Bool()) - } - case "ice-lite": - if in.IsNull() { - in.Skip() - } else { - out.ICELite = bool(in.Bool()) - } - case "ice-tcp": - if in.IsNull() { - in.Skip() - } else { - out.ICETCP = bool(in.Bool()) - } - case "ice-nomination": - if in.IsNull() { - in.Skip() - } else { - out.ICENomination = string(in.String()) - } - case "ice-consent-freshness": - if in.IsNull() { - in.Skip() - } else { - out.ICEConsentFreshness = bool(in.Bool()) - } - case "ice-keepalive-conncheck": - if in.IsNull() { - in.Skip() - } else { - out.ICEKeepaliveConncheck = bool(in.Bool()) - } - case "hangup-on-failed": - if in.IsNull() { - in.Skip() - } else { - out.HangupOnFailed = bool(in.Bool()) - } - case "full-trickle": - if in.IsNull() { - in.Skip() - } else { - out.FullTrickle = bool(in.Bool()) - } - case "mdns-enabled": - if in.IsNull() { - in.Skip() - } else { - out.MDNSEnabled = bool(in.Bool()) - } - case "min-nack-queue": - if in.IsNull() { - in.Skip() - } else { - out.MinNACKQueue = int(in.Int()) - } - case "nack-optimizations": - if in.IsNull() { - in.Skip() - } else { - out.NACKOptimizations = bool(in.Bool()) - } - case "twcc-period": - if in.IsNull() { - in.Skip() - } else { - out.TWCCPeriod = int(in.Int()) - } - case "dscp": - if in.IsNull() { - in.Skip() - } else { - out.DSCP = int(in.Int()) - } - case "dtls-mcu": - if in.IsNull() { - in.Skip() - } else { - out.DTLSMCU = int(in.Int()) - } - case "stun-server": - if in.IsNull() { - in.Skip() - } else { - out.STUNServer = string(in.String()) - } - case "turn-server": - if in.IsNull() { - in.Skip() - } else { - out.TURNServer = string(in.String()) - } - case "allow-force-relay": - if in.IsNull() { - in.Skip() - } else { - out.AllowForceRelay = bool(in.Bool()) - } - case "static-event-loops": - if in.IsNull() { - in.Skip() - } else { - out.StaticEventLoops = int(in.Int()) - } - case "loop-indication": - if in.IsNull() { - in.Skip() - } else { - out.LoopIndication = bool(in.Bool()) - } - case "api_secret": - if in.IsNull() { - in.Skip() - } else { - out.APISecret = bool(in.Bool()) - } - case "auth_token": - if in.IsNull() { - in.Skip() - } else { - out.AuthToken = bool(in.Bool()) - } - case "event_handlers": - if in.IsNull() { - in.Skip() - } else { - out.EventHandlers = bool(in.Bool()) - } - case "opaqueid_in_api": - if in.IsNull() { - in.Skip() - } else { - out.OpaqueIdInAPI = bool(in.Bool()) - } - case "webrtc_encryption": - if in.IsNull() { - in.Skip() - } else { - out.WebRTCEncryption = bool(in.Bool()) - } - case "dependencies": - if in.IsNull() { - in.Skip() - out.Dependencies = nil - } else { - if out.Dependencies == nil { - out.Dependencies = new(janusEventDependenciesInfo) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Dependencies).UnmarshalEasyJSON(in) - } - } - case "transports": - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - out.Transports = make(map[string]janusEventPluginInfo) - } else { - out.Transports = nil - } - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v2 janusEventPluginInfo - if in.IsNull() { - in.Skip() - } else { - (v2).UnmarshalEasyJSON(in) - } - (out.Transports)[key] = v2 - in.WantComma() - } - in.Delim('}') - } - case "events": - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - out.Events = make(map[string]janusEventPluginInfo) - } else { - out.Events = nil - } - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v3 janusEventPluginInfo - if in.IsNull() { - in.Skip() - } else { - (v3).UnmarshalEasyJSON(in) - } - (out.Events)[key] = v3 - in.WantComma() - } - in.Delim('}') - } - case "loggers": - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - out.Loggers = make(map[string]janusEventPluginInfo) - } else { - out.Loggers = nil - } - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v4 janusEventPluginInfo - if in.IsNull() { - in.Skip() - } else { - (v4).UnmarshalEasyJSON(in) - } - (out.Loggers)[key] = v4 - in.WantComma() - } - in.Delim('}') - } - case "plugins": - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - out.Plugins = make(map[string]janusEventPluginInfo) - } else { - out.Plugins = nil - } - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v5 janusEventPluginInfo - if in.IsNull() { - in.Skip() - } else { - (v5).UnmarshalEasyJSON(in) - } - (out.Plugins)[key] = v5 - in.WantComma() - } - in.Delim('}') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus10(out *jwriter.Writer, in janusEventStatusStartupInfo) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"janus\":" - out.RawString(prefix[1:]) - out.String(string(in.Janus)) - } - { - const prefix string = ",\"version\":" - out.RawString(prefix) - out.Int(int(in.Version)) - } - { - const prefix string = ",\"version_string\":" - out.RawString(prefix) - out.String(string(in.VersionString)) - } - { - const prefix string = ",\"author\":" - out.RawString(prefix) - out.String(string(in.Author)) - } - { - const prefix string = ",\"commit-hash\":" - out.RawString(prefix) - out.String(string(in.CommitHash)) - } - { - const prefix string = ",\"compile-time\":" - out.RawString(prefix) - out.String(string(in.CompileTime)) - } - { - const prefix string = ",\"log-to-stdout\":" - out.RawString(prefix) - out.Bool(bool(in.LogToStdout)) - } - { - const prefix string = ",\"log-to-file\":" - out.RawString(prefix) - out.Bool(bool(in.LogToFile)) - } - if in.LogPath != "" { - const prefix string = ",\"log-path\":" - out.RawString(prefix) - out.String(string(in.LogPath)) - } - { - const prefix string = ",\"data_channels\":" - out.RawString(prefix) - out.Bool(bool(in.DataChannels)) - } - { - const prefix string = ",\"accepting-new-sessions\":" - out.RawString(prefix) - out.Bool(bool(in.AcceptingNewSessions)) - } - { - const prefix string = ",\"session-timeout\":" - out.RawString(prefix) - out.Int(int(in.SessionTimeout)) - } - { - const prefix string = ",\"reclaim-session-timeout\":" - out.RawString(prefix) - out.Int(int(in.ReclaimSessionTimeout)) - } - { - const prefix string = ",\"candidates-timeout\":" - out.RawString(prefix) - out.Int(int(in.CandidatesTimeout)) - } - { - const prefix string = ",\"server-name\":" - out.RawString(prefix) - out.String(string(in.ServerName)) - } - { - const prefix string = ",\"local-ip\":" - out.RawString(prefix) - out.String(string(in.LocalIP)) - } - if in.PublicIP != "" { - const prefix string = ",\"public-ip\":" - out.RawString(prefix) - out.String(string(in.PublicIP)) - } - if len(in.PublicIPs) != 0 { - const prefix string = ",\"public-ips\":" - out.RawString(prefix) - { - out.RawByte('[') - for v6, v7 := range in.PublicIPs { - if v6 > 0 { - out.RawByte(',') - } - out.String(string(v7)) - } - out.RawByte(']') - } - } - { - const prefix string = ",\"ipv6\":" - out.RawString(prefix) - out.Bool(bool(in.IPv6)) - } - if in.IPv6LinkLocal { - const prefix string = ",\"ipv6-link-local\":" - out.RawString(prefix) - out.Bool(bool(in.IPv6LinkLocal)) - } - { - const prefix string = ",\"ice-lite\":" - out.RawString(prefix) - out.Bool(bool(in.ICELite)) - } - { - const prefix string = ",\"ice-tcp\":" - out.RawString(prefix) - out.Bool(bool(in.ICETCP)) - } - if in.ICENomination != "" { - const prefix string = ",\"ice-nomination\":" - out.RawString(prefix) - out.String(string(in.ICENomination)) - } - { - const prefix string = ",\"ice-consent-freshness\":" - out.RawString(prefix) - out.Bool(bool(in.ICEConsentFreshness)) - } - { - const prefix string = ",\"ice-keepalive-conncheck\":" - out.RawString(prefix) - out.Bool(bool(in.ICEKeepaliveConncheck)) - } - { - const prefix string = ",\"hangup-on-failed\":" - out.RawString(prefix) - out.Bool(bool(in.HangupOnFailed)) - } - { - const prefix string = ",\"full-trickle\":" - out.RawString(prefix) - out.Bool(bool(in.FullTrickle)) - } - { - const prefix string = ",\"mdns-enabled\":" - out.RawString(prefix) - out.Bool(bool(in.MDNSEnabled)) - } - { - const prefix string = ",\"min-nack-queue\":" - out.RawString(prefix) - out.Int(int(in.MinNACKQueue)) - } - { - const prefix string = ",\"nack-optimizations\":" - out.RawString(prefix) - out.Bool(bool(in.NACKOptimizations)) - } - { - const prefix string = ",\"twcc-period\":" - out.RawString(prefix) - out.Int(int(in.TWCCPeriod)) - } - if in.DSCP != 0 { - const prefix string = ",\"dscp\":" - out.RawString(prefix) - out.Int(int(in.DSCP)) - } - { - const prefix string = ",\"dtls-mcu\":" - out.RawString(prefix) - out.Int(int(in.DTLSMCU)) - } - if in.STUNServer != "" { - const prefix string = ",\"stun-server\":" - out.RawString(prefix) - out.String(string(in.STUNServer)) - } - if in.TURNServer != "" { - const prefix string = ",\"turn-server\":" - out.RawString(prefix) - out.String(string(in.TURNServer)) - } - if in.AllowForceRelay { - const prefix string = ",\"allow-force-relay\":" - out.RawString(prefix) - out.Bool(bool(in.AllowForceRelay)) - } - { - const prefix string = ",\"static-event-loops\":" - out.RawString(prefix) - out.Int(int(in.StaticEventLoops)) - } - if in.LoopIndication { - const prefix string = ",\"loop-indication\":" - out.RawString(prefix) - out.Bool(bool(in.LoopIndication)) - } - { - const prefix string = ",\"api_secret\":" - out.RawString(prefix) - out.Bool(bool(in.APISecret)) - } - { - const prefix string = ",\"auth_token\":" - out.RawString(prefix) - out.Bool(bool(in.AuthToken)) - } - { - const prefix string = ",\"event_handlers\":" - out.RawString(prefix) - out.Bool(bool(in.EventHandlers)) - } - { - const prefix string = ",\"opaqueid_in_api\":" - out.RawString(prefix) - out.Bool(bool(in.OpaqueIdInAPI)) - } - { - const prefix string = ",\"webrtc_encryption\":" - out.RawString(prefix) - out.Bool(bool(in.WebRTCEncryption)) - } - if in.Dependencies != nil { - const prefix string = ",\"dependencies\":" - out.RawString(prefix) - (*in.Dependencies).MarshalEasyJSON(out) - } - if len(in.Transports) != 0 { - const prefix string = ",\"transports\":" - out.RawString(prefix) - { - out.RawByte('{') - v8First := true - for v8Name, v8Value := range in.Transports { - if v8First { - v8First = false - } else { - out.RawByte(',') - } - out.String(string(v8Name)) - out.RawByte(':') - (v8Value).MarshalEasyJSON(out) - } - out.RawByte('}') - } - } - if len(in.Events) != 0 { - const prefix string = ",\"events\":" - out.RawString(prefix) - { - out.RawByte('{') - v9First := true - for v9Name, v9Value := range in.Events { - if v9First { - v9First = false - } else { - out.RawByte(',') - } - out.String(string(v9Name)) - out.RawByte(':') - (v9Value).MarshalEasyJSON(out) - } - out.RawByte('}') - } - } - if len(in.Loggers) != 0 { - const prefix string = ",\"loggers\":" - out.RawString(prefix) - { - out.RawByte('{') - v10First := true - for v10Name, v10Value := range in.Loggers { - if v10First { - v10First = false - } else { - out.RawByte(',') - } - out.String(string(v10Name)) - out.RawByte(':') - (v10Value).MarshalEasyJSON(out) - } - out.RawByte('}') - } - } - if len(in.Plugins) != 0 { - const prefix string = ",\"plugins\":" - out.RawString(prefix) - { - out.RawByte('{') - v11First := true - for v11Name, v11Value := range in.Plugins { - if v11First { - v11First = false - } else { - out.RawByte(',') - } - out.String(string(v11Name)) - out.RawByte(':') - (v11Value).MarshalEasyJSON(out) - } - out.RawByte('}') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventStatusStartupInfo) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus10(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventStatusStartupInfo) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus10(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventStatusStartupInfo) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus10(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventStatusStartupInfo) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus10(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus11(in *jlexer.Lexer, out *janusEventSessionTransport) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "transport": - if in.IsNull() { - in.Skip() - } else { - out.Transport = string(in.String()) - } - case "id": - if in.IsNull() { - in.Skip() - } else { - out.ID = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus11(out *jwriter.Writer, in janusEventSessionTransport) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"transport\":" - out.RawString(prefix[1:]) - out.String(string(in.Transport)) - } - { - const prefix string = ",\"id\":" - out.RawString(prefix) - out.String(string(in.ID)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventSessionTransport) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus11(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventSessionTransport) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus11(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventSessionTransport) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus11(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventSessionTransport) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus11(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus12(in *jlexer.Lexer, out *janusEventSession) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "name": - if in.IsNull() { - in.Skip() - } else { - out.Name = string(in.String()) - } - case "transport": - if in.IsNull() { - in.Skip() - out.Transport = nil - } else { - if out.Transport == nil { - out.Transport = new(janusEventSessionTransport) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Transport).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus12(out *jwriter.Writer, in janusEventSession) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"name\":" - out.RawString(prefix[1:]) - out.String(string(in.Name)) - } - if in.Transport != nil { - const prefix string = ",\"transport\":" - out.RawString(prefix) - (*in.Transport).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventSession) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus12(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventSession) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus12(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventSession) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus12(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventSession) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus12(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus13(in *jlexer.Lexer, out *janusEventPluginInfo) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "name": - if in.IsNull() { - in.Skip() - } else { - out.Name = string(in.String()) - } - case "author": - if in.IsNull() { - in.Skip() - } else { - out.Author = string(in.String()) - } - case "description": - if in.IsNull() { - in.Skip() - } else { - out.Description = string(in.String()) - } - case "version_string": - if in.IsNull() { - in.Skip() - } else { - out.VersionString = string(in.String()) - } - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus13(out *jwriter.Writer, in janusEventPluginInfo) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"name\":" - out.RawString(prefix[1:]) - out.String(string(in.Name)) - } - { - const prefix string = ",\"author\":" - out.RawString(prefix) - out.String(string(in.Author)) - } - { - const prefix string = ",\"description\":" - out.RawString(prefix) - out.String(string(in.Description)) - } - { - const prefix string = ",\"version_string\":" - out.RawString(prefix) - out.String(string(in.VersionString)) - } - { - const prefix string = ",\"version\":" - out.RawString(prefix) - out.Int(int(in.Version)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventPluginInfo) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus13(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventPluginInfo) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus13(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventPluginInfo) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus13(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventPluginInfo) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus13(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus14(in *jlexer.Lexer, out *janusEventPlugin) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "plugin": - if in.IsNull() { - in.Skip() - } else { - out.Plugin = string(in.String()) - } - case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus14(out *jwriter.Writer, in janusEventPlugin) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"plugin\":" - out.RawString(prefix[1:]) - out.String(string(in.Plugin)) - } - { - const prefix string = ",\"data\":" - out.RawString(prefix) - out.Raw((in.Data).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventPlugin) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus14(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventPlugin) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus14(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventPlugin) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus14(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventPlugin) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus14(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus15(in *jlexer.Lexer, out *janusEventMediaStats) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "mid": - if in.IsNull() { - in.Skip() - } else { - out.MID = string(in.String()) - } - case "mindex": - if in.IsNull() { - in.Skip() - } else { - out.MIndex = int(in.Int()) - } - case "media": - if in.IsNull() { - in.Skip() - } else { - out.Media = string(in.String()) - } - case "codec": - if in.IsNull() { - in.Skip() - } else { - out.Codec = string(in.String()) - } - case "base": - if in.IsNull() { - in.Skip() - } else { - out.Base = uint32(in.Uint32()) - } - case "lost": - if in.IsNull() { - in.Skip() - } else { - out.Lost = int32(in.Int32()) - } - case "lost-by-remote": - if in.IsNull() { - in.Skip() - } else { - out.LostByRemote = int32(in.Int32()) - } - case "jitter-local": - if in.IsNull() { - in.Skip() - } else { - out.JitterLocal = uint32(in.Uint32()) - } - case "jitter-remote": - if in.IsNull() { - in.Skip() - } else { - out.JitterRemote = uint32(in.Uint32()) - } - case "in-link-quality": - if in.IsNull() { - in.Skip() - } else { - out.InLinkQuality = uint32(in.Uint32()) - } - case "in-media-link-quality": - if in.IsNull() { - in.Skip() - } else { - out.InMediaLinkQuality = uint32(in.Uint32()) - } - case "out-link-quality": - if in.IsNull() { - in.Skip() - } else { - out.OutLinkQuality = uint32(in.Uint32()) - } - case "out-media-link-quality": - if in.IsNull() { - in.Skip() - } else { - out.OutMediaLinkQuality = uint32(in.Uint32()) - } - case "bytes-received-lastsec": - if in.IsNull() { - in.Skip() - } else { - out.BytesReceivedLastSec = uint32(in.Uint32()) - } - case "bytes-sent-lastsec": - if in.IsNull() { - in.Skip() - } else { - out.BytesSentLastSec = uint32(in.Uint32()) - } - case "nacks-received": - if in.IsNull() { - in.Skip() - } else { - out.NacksReceived = uint32(in.Uint32()) - } - case "nacks-sent": - if in.IsNull() { - in.Skip() - } else { - out.NacksSent = uint32(in.Uint32()) - } - case "retransmissions-received": - if in.IsNull() { - in.Skip() - } else { - out.RetransmissionsReceived = uint32(in.Uint32()) - } - case "rtt": - if in.IsNull() { - in.Skip() - } else { - out.RTT = uint32(in.Uint32()) - } - case "rtt-values": - if in.IsNull() { - in.Skip() - out.RTTValues = nil - } else { - if out.RTTValues == nil { - out.RTTValues = new(janusMediaStatsRTTValues) - } - if in.IsNull() { - in.Skip() - } else { - (*out.RTTValues).UnmarshalEasyJSON(in) - } - } - case "packets-received": - if in.IsNull() { - in.Skip() - } else { - out.PacketsReceived = uint32(in.Uint32()) - } - case "packets-sent": - if in.IsNull() { - in.Skip() - } else { - out.PacketsSent = uint32(in.Uint32()) - } - case "bytes-received": - if in.IsNull() { - in.Skip() - } else { - out.BytesReceived = uint64(in.Uint64()) - } - case "bytes-sent": - if in.IsNull() { - in.Skip() - } else { - out.BytesSent = uint64(in.Uint64()) - } - case "remb-bitrate": - if in.IsNull() { - in.Skip() - } else { - out.REMBBitrate = uint32(in.Uint32()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus15(out *jwriter.Writer, in janusEventMediaStats) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"mid\":" - out.RawString(prefix[1:]) - out.String(string(in.MID)) - } - { - const prefix string = ",\"mindex\":" - out.RawString(prefix) - out.Int(int(in.MIndex)) - } - { - const prefix string = ",\"media\":" - out.RawString(prefix) - out.String(string(in.Media)) - } - if in.Codec != "" { - const prefix string = ",\"codec\":" - out.RawString(prefix) - out.String(string(in.Codec)) - } - { - const prefix string = ",\"base\":" - out.RawString(prefix) - out.Uint32(uint32(in.Base)) - } - { - const prefix string = ",\"lost\":" - out.RawString(prefix) - out.Int32(int32(in.Lost)) - } - { - const prefix string = ",\"lost-by-remote\":" - out.RawString(prefix) - out.Int32(int32(in.LostByRemote)) - } - { - const prefix string = ",\"jitter-local\":" - out.RawString(prefix) - out.Uint32(uint32(in.JitterLocal)) - } - { - const prefix string = ",\"jitter-remote\":" - out.RawString(prefix) - out.Uint32(uint32(in.JitterRemote)) - } - { - const prefix string = ",\"in-link-quality\":" - out.RawString(prefix) - out.Uint32(uint32(in.InLinkQuality)) - } - { - const prefix string = ",\"in-media-link-quality\":" - out.RawString(prefix) - out.Uint32(uint32(in.InMediaLinkQuality)) - } - { - const prefix string = ",\"out-link-quality\":" - out.RawString(prefix) - out.Uint32(uint32(in.OutLinkQuality)) - } - { - const prefix string = ",\"out-media-link-quality\":" - out.RawString(prefix) - out.Uint32(uint32(in.OutMediaLinkQuality)) - } - { - const prefix string = ",\"bytes-received-lastsec\":" - out.RawString(prefix) - out.Uint32(uint32(in.BytesReceivedLastSec)) - } - { - const prefix string = ",\"bytes-sent-lastsec\":" - out.RawString(prefix) - out.Uint32(uint32(in.BytesSentLastSec)) - } - { - const prefix string = ",\"nacks-received\":" - out.RawString(prefix) - out.Uint32(uint32(in.NacksReceived)) - } - { - const prefix string = ",\"nacks-sent\":" - out.RawString(prefix) - out.Uint32(uint32(in.NacksSent)) - } - { - const prefix string = ",\"retransmissions-received\":" - out.RawString(prefix) - out.Uint32(uint32(in.RetransmissionsReceived)) - } - if in.RTT != 0 { - const prefix string = ",\"rtt\":" - out.RawString(prefix) - out.Uint32(uint32(in.RTT)) - } - if in.RTTValues != nil { - const prefix string = ",\"rtt-values\":" - out.RawString(prefix) - (*in.RTTValues).MarshalEasyJSON(out) - } - { - const prefix string = ",\"packets-received\":" - out.RawString(prefix) - out.Uint32(uint32(in.PacketsReceived)) - } - { - const prefix string = ",\"packets-sent\":" - out.RawString(prefix) - out.Uint32(uint32(in.PacketsSent)) - } - { - const prefix string = ",\"bytes-received\":" - out.RawString(prefix) - out.Uint64(uint64(in.BytesReceived)) - } - { - const prefix string = ",\"bytes-sent\":" - out.RawString(prefix) - out.Uint64(uint64(in.BytesSent)) - } - { - const prefix string = ",\"remb-bitrate\":" - out.RawString(prefix) - out.Uint32(uint32(in.REMBBitrate)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventMediaStats) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus15(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventMediaStats) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus15(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventMediaStats) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus15(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventMediaStats) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus15(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus16(in *jlexer.Lexer, out *janusEventMediaState) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "media": - if in.IsNull() { - in.Skip() - } else { - out.Media = string(in.String()) - } - case "mid": - if in.IsNull() { - in.Skip() - } else { - out.MID = string(in.String()) - } - case "substream": - if in.IsNull() { - in.Skip() - out.SubStream = nil - } else { - if out.SubStream == nil { - out.SubStream = new(int) - } - if in.IsNull() { - in.Skip() - } else { - *out.SubStream = int(in.Int()) - } - } - case "receiving": - if in.IsNull() { - in.Skip() - } else { - out.Receiving = bool(in.Bool()) - } - case "seconds": - if in.IsNull() { - in.Skip() - } else { - out.Seconds = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus16(out *jwriter.Writer, in janusEventMediaState) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"media\":" - out.RawString(prefix[1:]) - out.String(string(in.Media)) - } - { - const prefix string = ",\"mid\":" - out.RawString(prefix) - out.String(string(in.MID)) - } - if in.SubStream != nil { - const prefix string = ",\"substream\":" - out.RawString(prefix) - out.Int(int(*in.SubStream)) - } - { - const prefix string = ",\"receiving\":" - out.RawString(prefix) - out.Bool(bool(in.Receiving)) - } - { - const prefix string = ",\"seconds\":" - out.RawString(prefix) - out.Int(int(in.Seconds)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventMediaState) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus16(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventMediaState) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus16(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventMediaState) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus16(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventMediaState) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus16(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus17(in *jlexer.Lexer, out *janusEventMediaSlowLink) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "media": - if in.IsNull() { - in.Skip() - } else { - out.Media = string(in.String()) - } - case "mid": - if in.IsNull() { - in.Skip() - } else { - out.MID = string(in.String()) - } - case "slow_link": - if in.IsNull() { - in.Skip() - } else { - out.SlowLink = string(in.String()) - } - case "lost_lastsec": - if in.IsNull() { - in.Skip() - } else { - out.LostLastSec = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus17(out *jwriter.Writer, in janusEventMediaSlowLink) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"media\":" - out.RawString(prefix[1:]) - out.String(string(in.Media)) - } - { - const prefix string = ",\"mid\":" - out.RawString(prefix) - out.String(string(in.MID)) - } - { - const prefix string = ",\"slow_link\":" - out.RawString(prefix) - out.String(string(in.SlowLink)) - } - { - const prefix string = ",\"lost_lastsec\":" - out.RawString(prefix) - out.Int(int(in.LostLastSec)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventMediaSlowLink) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus17(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventMediaSlowLink) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus17(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventMediaSlowLink) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus17(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventMediaSlowLink) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus17(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus18(in *jlexer.Lexer, out *janusEventJSEP) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "owner": - if in.IsNull() { - in.Skip() - } else { - out.Owner = string(in.String()) - } - case "jsep": - easyjsonC1cedd36Decode(in, &out.Jsep) - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus18(out *jwriter.Writer, in janusEventJSEP) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"owner\":" - out.RawString(prefix[1:]) - out.String(string(in.Owner)) - } - { - const prefix string = ",\"jsep\":" - out.RawString(prefix) - easyjsonC1cedd36Encode(out, in.Jsep) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventJSEP) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus18(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventJSEP) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus18(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventJSEP) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus18(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventJSEP) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus18(l, v) -} -func easyjsonC1cedd36Decode(in *jlexer.Lexer, out *struct { - Type string `json:"type"` - SDP string `json:"sdp"` -}) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } - case "sdp": - if in.IsNull() { - in.Skip() - } else { - out.SDP = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36Encode(out *jwriter.Writer, in struct { - Type string `json:"type"` - SDP string `json:"sdp"` -}) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"type\":" - out.RawString(prefix[1:]) - out.String(string(in.Type)) - } - { - const prefix string = ",\"sdp\":" - out.RawString(prefix) - out.String(string(in.SDP)) - } - out.RawByte('}') -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus19(in *jlexer.Lexer, out *janusEventHandle) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "name": - if in.IsNull() { - in.Skip() - } else { - out.Name = string(in.String()) - } - case "plugin": - if in.IsNull() { - in.Skip() - } else { - out.Plugin = string(in.String()) - } - case "token": - if in.IsNull() { - in.Skip() - } else { - out.Token = string(in.String()) - } - case "opaque_id": - if in.IsNull() { - in.Skip() - } else { - out.OpaqueId = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus19(out *jwriter.Writer, in janusEventHandle) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"name\":" - out.RawString(prefix[1:]) - out.String(string(in.Name)) - } - { - const prefix string = ",\"plugin\":" - out.RawString(prefix) - out.String(string(in.Plugin)) - } - if in.Token != "" { - const prefix string = ",\"token\":" - out.RawString(prefix) - out.String(string(in.Token)) - } - if in.OpaqueId != "" { - const prefix string = ",\"opaque_id\":" - out.RawString(prefix) - out.String(string(in.OpaqueId)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventHandle) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus19(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventHandle) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus19(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventHandle) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus19(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventHandle) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus19(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus20(in *jlexer.Lexer, out *janusEventExternal) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "schema": - if in.IsNull() { - in.Skip() - } else { - out.Schema = string(in.String()) - } - case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus20(out *jwriter.Writer, in janusEventExternal) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"schema\":" - out.RawString(prefix[1:]) - out.String(string(in.Schema)) - } - { - const prefix string = ",\"data\":" - out.RawString(prefix) - out.Raw((in.Data).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventExternal) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus20(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventExternal) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus20(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventExternal) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus20(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventExternal) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus20(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus21(in *jlexer.Lexer, out *janusEventDependenciesInfo) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "glib2": - if in.IsNull() { - in.Skip() - } else { - out.Glib2 = string(in.String()) - } - case "jansson": - if in.IsNull() { - in.Skip() - } else { - out.Jansson = string(in.String()) - } - case "libnice": - if in.IsNull() { - in.Skip() - } else { - out.Libnice = string(in.String()) - } - case "libsrtp": - if in.IsNull() { - in.Skip() - } else { - out.Libsrtp = string(in.String()) - } - case "libcurl": - if in.IsNull() { - in.Skip() - } else { - out.Libcurl = string(in.String()) - } - case "crypto": - if in.IsNull() { - in.Skip() - } else { - out.Crypto = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus21(out *jwriter.Writer, in janusEventDependenciesInfo) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"glib2\":" - out.RawString(prefix[1:]) - out.String(string(in.Glib2)) - } - { - const prefix string = ",\"jansson\":" - out.RawString(prefix) - out.String(string(in.Jansson)) - } - { - const prefix string = ",\"libnice\":" - out.RawString(prefix) - out.String(string(in.Libnice)) - } - { - const prefix string = ",\"libsrtp\":" - out.RawString(prefix) - out.String(string(in.Libsrtp)) - } - if in.Libcurl != "" { - const prefix string = ",\"libcurl\":" - out.RawString(prefix) - out.String(string(in.Libcurl)) - } - { - const prefix string = ",\"crypto\":" - out.RawString(prefix) - out.String(string(in.Crypto)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventDependenciesInfo) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus21(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventDependenciesInfo) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus21(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventDependenciesInfo) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus21(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventDependenciesInfo) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus21(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus22(in *jlexer.Lexer, out *janusEventCoreStartup) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "status": - if in.IsNull() { - in.Skip() - } else { - out.Status = string(in.String()) - } - case "info": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Info).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus22(out *jwriter.Writer, in janusEventCoreStartup) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"status\":" - out.RawString(prefix[1:]) - out.String(string(in.Status)) - } - { - const prefix string = ",\"info\":" - out.RawString(prefix) - out.Raw((in.Info).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventCoreStartup) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus22(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventCoreStartup) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus22(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventCoreStartup) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus22(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventCoreStartup) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus22(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus23(in *jlexer.Lexer, out *janusEventCoreShutdown) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "status": - if in.IsNull() { - in.Skip() - } else { - out.Status = string(in.String()) - } - case "signum": - if in.IsNull() { - in.Skip() - } else { - out.Signum = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus23(out *jwriter.Writer, in janusEventCoreShutdown) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"status\":" - out.RawString(prefix[1:]) - out.String(string(in.Status)) - } - { - const prefix string = ",\"signum\":" - out.RawString(prefix) - out.Int(int(in.Signum)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventCoreShutdown) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus23(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventCoreShutdown) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus23(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventCoreShutdown) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus23(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventCoreShutdown) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus23(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus24(in *jlexer.Lexer, out *janusEventCandidates) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "local": - if in.IsNull() { - in.Skip() - } else { - (out.Local).UnmarshalEasyJSON(in) - } - case "remote": - if in.IsNull() { - in.Skip() - } else { - (out.Remote).UnmarshalEasyJSON(in) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus24(out *jwriter.Writer, in janusEventCandidates) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"local\":" - out.RawString(prefix[1:]) - (in.Local).MarshalEasyJSON(out) - } - { - const prefix string = ",\"remote\":" - out.RawString(prefix) - (in.Remote).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventCandidates) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus24(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventCandidates) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus24(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventCandidates) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus24(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventCandidates) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus24(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus25(in *jlexer.Lexer, out *janusEventCandidate) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "address": - if in.IsNull() { - in.Skip() - } else { - out.Address = string(in.String()) - } - case "port": - if in.IsNull() { - in.Skip() - } else { - out.Port = int(in.Int()) - } - case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } - case "transport": - if in.IsNull() { - in.Skip() - } else { - out.Transport = string(in.String()) - } - case "family": - if in.IsNull() { - in.Skip() - } else { - out.Family = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus25(out *jwriter.Writer, in janusEventCandidate) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"address\":" - out.RawString(prefix[1:]) - out.String(string(in.Address)) - } - { - const prefix string = ",\"port\":" - out.RawString(prefix) - out.Int(int(in.Port)) - } - { - const prefix string = ",\"type\":" - out.RawString(prefix) - out.String(string(in.Type)) - } - { - const prefix string = ",\"transport\":" - out.RawString(prefix) - out.String(string(in.Transport)) - } - { - const prefix string = ",\"family\":" - out.RawString(prefix) - out.Int(int(in.Family)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEventCandidate) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus25(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEventCandidate) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus25(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEventCandidate) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus25(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEventCandidate) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus25(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus26(in *jlexer.Lexer, out *janusEvent) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "emitter": - if in.IsNull() { - in.Skip() - } else { - out.Emitter = string(in.String()) - } - case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = int(in.Int()) - } - case "subtype": - if in.IsNull() { - in.Skip() - } else { - out.SubType = int(in.Int()) - } - case "timestamp": - if in.IsNull() { - in.Skip() - } else { - out.Timestamp = uint64(in.Uint64()) - } - case "session_id": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = uint64(in.Uint64()) - } - case "handle_id": - if in.IsNull() { - in.Skip() - } else { - out.HandleId = uint64(in.Uint64()) - } - case "opaque_id": - if in.IsNull() { - in.Skip() - } else { - out.OpaqueId = uint64(in.Uint64()) - } - case "event": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Event).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus26(out *jwriter.Writer, in janusEvent) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"emitter\":" - out.RawString(prefix[1:]) - out.String(string(in.Emitter)) - } - { - const prefix string = ",\"type\":" - out.RawString(prefix) - out.Int(int(in.Type)) - } - if in.SubType != 0 { - const prefix string = ",\"subtype\":" - out.RawString(prefix) - out.Int(int(in.SubType)) - } - { - const prefix string = ",\"timestamp\":" - out.RawString(prefix) - out.Uint64(uint64(in.Timestamp)) - } - if in.SessionId != 0 { - const prefix string = ",\"session_id\":" - out.RawString(prefix) - out.Uint64(uint64(in.SessionId)) - } - if in.HandleId != 0 { - const prefix string = ",\"handle_id\":" - out.RawString(prefix) - out.Uint64(uint64(in.HandleId)) - } - if in.OpaqueId != 0 { - const prefix string = ",\"opaque_id\":" - out.RawString(prefix) - out.Uint64(uint64(in.OpaqueId)) - } - { - const prefix string = ",\"event\":" - out.RawString(prefix) - out.Raw((in.Event).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v janusEvent) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus26(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v janusEvent) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus26(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *janusEvent) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus26(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *janusEvent) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2SfuJanus26(l, v) -} diff --git a/sfu/janus/client.go b/sfu/janus/client.go deleted file mode 100644 index 04b3593..0000000 --- a/sfu/janus/client.go +++ /dev/null @@ -1,259 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "context" - "reflect" - "strconv" - "sync" - "sync/atomic" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" -) - -type janusClient struct { - logger log.Logger - mcu *janusSFU - listener sfu.Listener - mu sync.Mutex - - id uint64 - session uint64 - roomId uint64 - sid string - streamType sfu.StreamType - maxBitrate api.Bandwidth - - // +checklocks:mu - bandwidth map[string]*sfu.ClientBandwidthInfo - - handle atomic.Pointer[janus.Handle] - handleId atomic.Uint64 - closeChan chan struct{} - deferred chan func() - - handleEvent func(event *janus.EventMsg) - handleHangup func(event *janus.HangupMsg) - handleDetached func(event *janus.DetachedMsg) - handleConnected func(event *janus.WebRTCUpMsg) - handleSlowLink func(event *janus.SlowLinkMsg) - handleMedia func(event *janus.MediaMsg) -} - -func (c *janusClient) Id() string { - return strconv.FormatUint(c.id, 10) -} - -func (c *janusClient) Sid() string { - return c.sid -} - -func (c *janusClient) Handle() uint64 { - return c.handleId.Load() -} - -func (c *janusClient) StreamType() sfu.StreamType { - return c.streamType -} - -func (c *janusClient) MaxBitrate() api.Bandwidth { - return c.maxBitrate -} - -func (c *janusClient) Close(ctx context.Context) { -} - -func (c *janusClient) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { -} - -func (c *janusClient) UpdateBandwidth(media string, sent api.Bandwidth, received api.Bandwidth) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.bandwidth == nil { - c.bandwidth = make(map[string]*sfu.ClientBandwidthInfo) - } - - info, found := c.bandwidth[media] - if !found { - info = &sfu.ClientBandwidthInfo{} - c.bandwidth[media] = info - } - - info.Sent = sent - info.Received = received -} - -func (c *janusClient) Bandwidth() *sfu.ClientBandwidthInfo { - c.mu.Lock() - defer c.mu.Unlock() - - if c.bandwidth == nil { - return nil - } - - result := &sfu.ClientBandwidthInfo{} - for _, info := range c.bandwidth { - result.Received += info.Received - result.Sent += info.Sent - } - return result -} - -func (c *janusClient) closeClient(ctx context.Context) bool { - if handle := c.handle.Swap(nil); handle != nil { - close(c.closeChan) - if _, err := handle.Detach(ctx); err != nil { - if e, ok := err.(*janus.ErrorMsg); !ok || e.Err.Code != janus.JANUS_ERROR_HANDLE_NOT_FOUND { - c.logger.Println("Could not detach client", handle.Id, err) - } - } - return true - } - - return false -} - -func (c *janusClient) run(handle *janus.Handle, closeChan <-chan struct{}) { -loop: - for { - select { - case msg := <-handle.Events: - switch t := msg.(type) { - case *janus.EventMsg: - c.handleEvent(t) - case *janus.HangupMsg: - c.handleHangup(t) - case *janus.DetachedMsg: - c.handleDetached(t) - case *janus.MediaMsg: - c.handleMedia(t) - case *janus.WebRTCUpMsg: - c.handleConnected(t) - case *janus.SlowLinkMsg: - c.handleSlowLink(t) - case *janus.TrickleMsg: - c.handleTrickle(t) - default: - c.logger.Println("Received unsupported event type", msg, reflect.TypeOf(msg)) - } - case f := <-c.deferred: - f() - case <-closeChan: - break loop - } - } -} - -func (c *janusClient) sendOffer(ctx context.Context, offer api.StringMap, callback func(error, api.StringMap)) { - handle := c.handle.Load() - if handle == nil { - callback(sfu.ErrNotConnected, nil) - return - } - - configure_msg := api.StringMap{ - "request": "configure", - "audio": true, - "video": true, - "data": true, - } - answer_msg, err := handle.Message(ctx, configure_msg, offer) - if err != nil { - callback(err, nil) - return - } - - callback(nil, answer_msg.Jsep) -} - -func (c *janusClient) sendAnswer(ctx context.Context, answer api.StringMap, callback func(error, api.StringMap)) { - handle := c.handle.Load() - if handle == nil { - callback(sfu.ErrNotConnected, nil) - return - } - - start_msg := api.StringMap{ - "request": "start", - "room": c.roomId, - } - start_response, err := handle.Message(ctx, start_msg, answer) - if err != nil { - callback(err, nil) - return - } - - c.logger.Println("Started listener", start_response) - callback(nil, nil) -} - -func (c *janusClient) sendCandidate(ctx context.Context, candidate any, callback func(error, api.StringMap)) { - handle := c.handle.Load() - if handle == nil { - callback(sfu.ErrNotConnected, nil) - return - } - - if _, err := handle.Trickle(ctx, candidate); err != nil { - callback(err, nil) - return - } - callback(nil, nil) -} - -func (c *janusClient) handleTrickle(event *janus.TrickleMsg) { - if event.Candidate.Completed { - c.listener.OnIceCompleted(c) - } else { - c.listener.OnIceCandidate(c, event.Candidate) - } -} - -func (c *janusClient) selectStream(ctx context.Context, stream *streamSelection, callback func(error, api.StringMap)) { - handle := c.handle.Load() - if handle == nil { - callback(sfu.ErrNotConnected, nil) - return - } - - if stream == nil || !stream.HasValues() { - callback(nil, nil) - return - } - - configure_msg := api.StringMap{ - "request": "configure", - } - stream.AddToMessage(configure_msg) - _, err := handle.Message(ctx, configure_msg, nil) - if err != nil { - callback(err, nil) - return - } - - callback(nil, nil) -} diff --git a/sfu/janus/events_handler.go b/sfu/janus/events_handler.go deleted file mode 100644 index 37cd4f3..0000000 --- a/sfu/janus/events_handler.go +++ /dev/null @@ -1,461 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "math" - "net" - "strconv" - "strings" - "sync" - "time" - - "github.com/gorilla/websocket" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/pool" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" -) - -const ( - EventsSubprotocol = "janus-events" - - // Time allowed to write a message to the peer. - writeWait = 10 * time.Second - - // Time allowed to read the next pong message from the peer. - pongWait = 60 * time.Second - - // Send pings to peer with this period. Must be less than pongWait. - pingPeriod = (pongWait * 9) / 10 -) - -var ( - bufferPool pool.BufferPool -) - -type EventHandler interface { - UpdateBandwidth(handle uint64, media string, sent api.Bandwidth, received api.Bandwidth) -} - -type valueCounter struct { - values map[string]uint64 -} - -func (c *valueCounter) Update(key string, value uint64) uint64 { - if c.values == nil { - c.values = make(map[string]uint64) - } - - var delta uint64 - prev := c.values[key] - if value == prev { - return 0 - } else if value < prev { - // Wrap around - c.values[key] = 0 - delta = math.MaxUint64 - prev + value - } else { - delta = value - prev - } - - c.values[key] += delta - return delta -} - -type handleStats struct { - codecs map[string]string - - bytesReceived valueCounter - bytesSent valueCounter - - nacksReceived valueCounter - nacksSent valueCounter - - lostLocal valueCounter - lostRemote valueCounter - - retransmissionsReceived valueCounter -} - -func (h *handleStats) Codec(media string, codec string) { - if h.codecs == nil { - h.codecs = make(map[string]string) - } - if h.codecs[media] != codec { - statsJanusMediaCodecsTotal.WithLabelValues(media, codec).Inc() - h.codecs[media] = codec - } -} - -func (h *handleStats) BytesReceived(media string, bytes uint64) { - delta := h.bytesReceived.Update(media, bytes) - statsJanusMediaBytesTotal.WithLabelValues(media, "incoming").Add(float64(delta)) -} - -func (h *handleStats) BytesSent(media string, bytes uint64) { - delta := h.bytesSent.Update(media, bytes) - statsJanusMediaBytesTotal.WithLabelValues(media, "outgoing").Add(float64(delta)) -} - -func (h *handleStats) NacksReceived(media string, nacks uint64) { - delta := h.nacksReceived.Update(media, nacks) - statsJanusMediaNACKTotal.WithLabelValues(media, "incoming").Add(float64(delta)) -} - -func (h *handleStats) NacksSent(media string, nacks uint64) { - delta := h.nacksSent.Update(media, nacks) - statsJanusMediaNACKTotal.WithLabelValues(media, "outgoing").Add(float64(delta)) -} - -func (h *handleStats) RetransmissionsReceived(media string, retransmissions uint64) { - delta := h.retransmissionsReceived.Update(media, retransmissions) - statsJanusMediaRetransmissionsTotal.WithLabelValues(media).Add(float64(delta)) -} - -func (h *handleStats) LostLocal(media string, lost uint64) { - delta := h.lostLocal.Update(media, lost) - statsJanusMediaLostTotal.WithLabelValues(media, "local").Add(float64(delta)) -} - -func (h *handleStats) LostRemote(media string, lost uint64) { - delta := h.lostRemote.Update(media, lost) - statsJanusMediaLostTotal.WithLabelValues(media, "remote").Add(float64(delta)) -} - -type EventsHandler struct { - mu sync.Mutex - - logger log.Logger - ctx context.Context - mcu EventHandler - // +checklocks:mu - conn *websocket.Conn - addr string - agent string - - supportsHandles bool - handleStats map[uint64]*handleStats - - events chan janusEvent -} - -func RunEventsHandler(ctx context.Context, mcu sfu.SFU, conn *websocket.Conn, addr string, agent string) { - deadline := time.Now().Add(time.Second) - if mcu == nil { - conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "no mcu configured"), deadline) // nolint - return - } - - m, ok := mcu.(EventHandler) - if !ok { - conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "mcu does not support events"), deadline) // nolint - return - } - - if !internal.IsLoopbackIP(addr) && !internal.IsPrivateIP(addr) { - conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, "only loopback and private connections allowed"), deadline) // nolint - return - } - - client, err := NewEventsHandler(ctx, m, conn, addr, agent) - if err != nil { - logger := log.LoggerFromContext(ctx) - logger.Printf("Could not create Janus events handler for %s: %s", addr, err) - conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "error creating handler"), deadline) // nolint - return - } - - client.Run() -} - -func NewEventsHandler(ctx context.Context, mcu EventHandler, conn *websocket.Conn, addr string, agent string) (*EventsHandler, error) { - handler := &EventsHandler{ - logger: log.LoggerFromContext(ctx), - ctx: ctx, - mcu: mcu, - conn: conn, - addr: addr, - agent: agent, - - events: make(chan janusEvent, 1), - } - - return handler, nil -} - -func (h *EventsHandler) Run() { - h.logger.Printf("Processing Janus events from %s", h.addr) - go h.writePump() - go h.processEvents() - - h.readPump() -} - -func (h *EventsHandler) close() { - h.mu.Lock() - conn := h.conn - h.conn = nil - h.mu.Unlock() - - if conn != nil { - if err := conn.Close(); err != nil { - h.logger.Printf("Error closing %s", err) - } - } -} - -func (h *EventsHandler) readPump() { - h.mu.Lock() - conn := h.conn - h.mu.Unlock() - if conn == nil { - h.logger.Printf("Connection from %s closed while starting readPump", h.addr) - return - } - - conn.SetPongHandler(func(msg string) error { - now := time.Now() - conn.SetReadDeadline(now.Add(pongWait)) // nolint - return nil - }) - - for { - conn.SetReadDeadline(time.Now().Add(pongWait)) // nolint - messageType, reader, err := conn.NextReader() - if err != nil { - // Gorilla websocket hides the original net.Error, so also compare error messages - if errors.Is(err, net.ErrClosed) || errors.Is(err, websocket.ErrCloseSent) || strings.Contains(err.Error(), net.ErrClosed.Error()) { - break - } else if _, ok := err.(*websocket.CloseError); !ok || websocket.IsUnexpectedCloseError(err, - websocket.CloseNormalClosure, - websocket.CloseGoingAway, - websocket.CloseNoStatusReceived) { - h.logger.Printf("Error reading from %s: %v", h.addr, err) - } - break - } - - if messageType != websocket.TextMessage { - h.logger.Printf("Unsupported message type %v from %s", messageType, h.addr) - continue - } - - decodeBuffer, err := bufferPool.ReadAll(reader) - if err != nil { - h.logger.Printf("Error reading message from %s: %v", h.addr, err) - break - } - - if decodeBuffer.Len() == 0 { - h.logger.Printf("Received empty message from %s", h.addr) - bufferPool.Put(decodeBuffer) - break - } - - var events []janusEvent - if data := decodeBuffer.Bytes(); data[0] != '[' { - var event janusEvent - if err := json.Unmarshal(data, &event); err != nil { - h.logger.Printf("Error decoding message %s from %s: %v", decodeBuffer.String(), h.addr, err) - bufferPool.Put(decodeBuffer) - break - } - - events = append(events, event) - } else { - if err := json.Unmarshal(data, &events); err != nil { - h.logger.Printf("Error decoding message %s from %s: %v", decodeBuffer.String(), h.addr, err) - bufferPool.Put(decodeBuffer) - break - } - } - - bufferPool.Put(decodeBuffer) - for _, e := range events { - h.events <- e - } - } -} - -func (h *EventsHandler) sendPing() bool { - h.mu.Lock() - defer h.mu.Unlock() - if h.conn == nil { - return false - } - - now := time.Now().UnixNano() - msg := strconv.FormatInt(now, 10) - h.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint - if err := h.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil { - h.logger.Printf("Could not send ping to %s: %v", h.addr, err) - return false - } - - return true -} - -func (h *EventsHandler) writePump() { - ticker := time.NewTicker(pingPeriod) - defer func() { - ticker.Stop() - h.close() - }() - - for { - select { - case <-ticker.C: - if !h.sendPing() { - return - } - case <-h.ctx.Done(): - return - } - } -} - -func (h *EventsHandler) processEvents() { - for { - select { - case event := <-h.events: - h.processEvent(event) - case <-h.ctx.Done(): - return - } - } -} - -func (h *EventsHandler) deleteHandleStats(event janusEvent) { - if event.HandleId != 0 { - delete(h.handleStats, event.HandleId) - } -} - -func (h *EventsHandler) getHandleStats(event janusEvent) *handleStats { - if !h.supportsHandles { - // Only create per-handle stats if enabled in Janus. Otherwise the - // handleStats map will never be cleaned up. - return nil - } else if event.HandleId == 0 { - return nil - } - - if h.handleStats == nil { - h.handleStats = make(map[uint64]*handleStats) - } - stats, found := h.handleStats[event.HandleId] - if !found { - stats = &handleStats{} - h.handleStats[event.HandleId] = stats - } - return stats -} - -func (h *EventsHandler) processEvent(event janusEvent) { - evt, err := event.Decode() - if err != nil { - h.logger.Printf("Error decoding event %s (%s)", event, err) - return - } - - switch evt := evt.(type) { - case *janusEventHandle: - switch evt.Name { - case "attached": - h.supportsHandles = true - case "detached": - h.deleteHandleStats(event) - } - case *janusEventWebRTCICE: - statsJanusICEStateTotal.WithLabelValues(evt.ICE).Inc() - case *janusEventWebRTCDTLS: - statsJanusDTLSStateTotal.WithLabelValues(evt.DTLS).Inc() - case *janusEventWebRTCPeerConnection: - statsJanusPeerConnectionStateTotal.WithLabelValues(evt.Connection, evt.Reason).Inc() - case *janusEventWebRTCSelectedPair: - statsJanusSelectedCandidateTotal.WithLabelValues("local", evt.Candidates.Local.Type, evt.Candidates.Local.Transport, fmt.Sprintf("ipv%d", evt.Candidates.Local.Family)).Inc() - statsJanusSelectedCandidateTotal.WithLabelValues("remote", evt.Candidates.Remote.Type, evt.Candidates.Remote.Transport, fmt.Sprintf("ipv%d", evt.Candidates.Remote.Family)).Inc() - case *janusEventMediaSlowLink: - var direction string - // "uplink" is Janus -> client, "downlink" is client -> Janus. - if evt.SlowLink == "uplink" { - direction = "outgoing" - } else { - direction = "incoming" - } - statsJanusSlowLinkTotal.WithLabelValues(evt.Media, direction).Inc() - case *janusEventMediaStats: - if rtt := evt.RTT; rtt > 0 { - statsJanusMediaRTT.WithLabelValues(evt.Media).Observe(float64(rtt)) - } - if jitter := evt.JitterLocal; jitter > 0 { - statsJanusMediaJitter.WithLabelValues(evt.Media, "local").Observe(float64(jitter)) - } - if jitter := evt.JitterRemote; jitter > 0 { - statsJanusMediaJitter.WithLabelValues(evt.Media, "remote").Observe(float64(jitter)) - } - if codec := evt.Codec; codec != "" { - if stats := h.getHandleStats(event); stats != nil { - stats.Codec(evt.Media, codec) - } - } - if stats := h.getHandleStats(event); stats != nil { - stats.BytesReceived(evt.Media, evt.BytesReceived) - } - if stats := h.getHandleStats(event); stats != nil { - stats.BytesSent(evt.Media, evt.BytesSent) - } - if nacks := evt.NacksReceived; nacks > 0 { - if stats := h.getHandleStats(event); stats != nil { - stats.NacksReceived(evt.Media, uint64(nacks)) - } - } - if nacks := evt.NacksSent; nacks > 0 { - if stats := h.getHandleStats(event); stats != nil { - stats.NacksSent(evt.Media, uint64(nacks)) - } - } - if retransmissions := evt.RetransmissionsReceived; retransmissions > 0 { - if stats := h.getHandleStats(event); stats != nil { - stats.RetransmissionsReceived(evt.Media, uint64(retransmissions)) - } - } - if lost := evt.Lost; lost > 0 { - if stats := h.getHandleStats(event); stats != nil { - stats.LostLocal(evt.Media, uint64(lost)) - } - } - if lost := evt.LostByRemote; lost > 0 { - if stats := h.getHandleStats(event); stats != nil { - stats.LostRemote(evt.Media, uint64(lost)) - } - } - h.mcu.UpdateBandwidth(event.HandleId, evt.Media, api.BandwidthFromBytes(uint64(evt.BytesSentLastSec)), api.BandwidthFromBytes(uint64(evt.BytesReceivedLastSec))) - } -} diff --git a/sfu/janus/events_handler_test.go b/sfu/janus/events_handler_test.go deleted file mode 100644 index 346aaa2..0000000 --- a/sfu/janus/events_handler_test.go +++ /dev/null @@ -1,671 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "context" - "encoding/json" - "fmt" - "math" - "net" - "net/http" - "net/http/httptest" - "strings" - "sync" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - metricstest "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - sfutest "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/test" -) - -type TestJanusEventsServerHandler struct { - t *testing.T - upgrader websocket.Upgrader - - mcu sfu.SFU - addr string - wg sync.WaitGroup -} - -func (h *TestJanusEventsServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.t.Helper() - h.wg.Add(1) - defer h.wg.Done() - assert := assert.New(h.t) - conn, err := h.upgrader.Upgrade(w, r, nil) - assert.NoError(err) - - if conn.Subprotocol() == EventsSubprotocol { - addr := h.addr - if addr == "" { - addr = r.RemoteAddr - } - if host, _, err := net.SplitHostPort(addr); err == nil { - addr = host - } - logger := logtest.NewLoggerForTest(h.t) - ctx := log.NewLoggerContext(r.Context(), logger) - RunEventsHandler(ctx, h.mcu, conn, addr, r.Header.Get("User-Agent")) - return - } - - deadline := time.Now().Add(time.Second) - assert.NoError(conn.SetWriteDeadline(deadline)) - assert.NoError(conn.WriteJSON(map[string]string{"error": "invalid_subprotocol"})) - assert.NoError(conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseProtocolError, "invalid_subprotocol"), deadline)) - assert.NoError(conn.Close()) -} - -func NewTestJanusEventsHandlerServer(t *testing.T) (*httptest.Server, string, *TestJanusEventsServerHandler) { - t.Helper() - - handler := &TestJanusEventsServerHandler{ - t: t, - upgrader: websocket.Upgrader{ - Subprotocols: []string{ - EventsSubprotocol, - }, - }, - } - server := httptest.NewServer(handler) - t.Cleanup(func() { - server.Close() - server.CloseClientConnections() - handler.wg.Wait() - }) - url := strings.ReplaceAll(server.URL, "http://", "ws://") - url = strings.ReplaceAll(url, "https://", "wss://") - return server, url, handler -} - -func TestJanusEventsHandlerNoMcu(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - _, url, _ := NewTestJanusEventsHandlerServer(t) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - dialer := websocket.Dialer{ - Subprotocols: []string{ - EventsSubprotocol, - }, - } - conn, response, err := dialer.DialContext(ctx, url, nil) - require.NoError(err) - defer func() { - assert.NoError(conn.Close()) - }() - - assert.Equal(EventsSubprotocol, response.Header.Get("Sec-WebSocket-Protocol")) - - var ce *websocket.CloseError - require.NoError(conn.SetReadDeadline(time.Now().Add(testTimeout))) - if mt, msg, err := conn.ReadMessage(); err == nil { - assert.Fail("connection was not closed", "expected close error, got message %s with type %d", string(msg), mt) - } else if assert.ErrorAs(err, &ce) { - assert.Equal(websocket.CloseInternalServerErr, ce.Code) - assert.Equal("no mcu configured", ce.Text) - } -} - -func TestJanusEventsHandlerInvalidMcu(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - _, url, handler := NewTestJanusEventsHandlerServer(t) - - handler.mcu = sfutest.NewSFU(t) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - dialer := websocket.Dialer{ - Subprotocols: []string{ - EventsSubprotocol, - }, - } - conn, response, err := dialer.DialContext(ctx, url, nil) - require.NoError(err) - defer func() { - assert.NoError(conn.Close()) - }() - - assert.Equal(EventsSubprotocol, response.Header.Get("Sec-WebSocket-Protocol")) - - var ce *websocket.CloseError - require.NoError(conn.SetReadDeadline(time.Now().Add(testTimeout))) - if mt, msg, err := conn.ReadMessage(); err == nil { - assert.Fail("connection was not closed", "expected close error, got message %s with type %d", string(msg), mt) - } else if assert.ErrorAs(err, &ce) { - assert.Equal(websocket.CloseInternalServerErr, ce.Code) - assert.Equal("mcu does not support events", ce.Text) - } -} - -func TestJanusEventsHandlerPublicIP(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - _, url, handler := NewTestJanusEventsHandlerServer(t) - - handler.mcu = &janusSFU{} - handler.addr = "1.2.3.4" - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - dialer := websocket.Dialer{ - Subprotocols: []string{ - EventsSubprotocol, - }, - } - conn, response, err := dialer.DialContext(ctx, url, nil) - require.NoError(err) - defer func() { - assert.NoError(conn.Close()) - }() - - assert.Equal(EventsSubprotocol, response.Header.Get("Sec-WebSocket-Protocol")) - - var ce *websocket.CloseError - require.NoError(conn.SetReadDeadline(time.Now().Add(testTimeout))) - if mt, msg, err := conn.ReadMessage(); err == nil { - assert.Fail("connection was not closed", "expected close error, got message %s with type %d", string(msg), mt) - } else if assert.ErrorAs(err, &ce) { - assert.Equal(websocket.ClosePolicyViolation, ce.Code) - assert.Equal("only loopback and private connections allowed", ce.Text) - } -} - -type TestMcuWithEvents struct { - sfutest.SFU - - t *testing.T - mu sync.Mutex - // +checklocks:mu - idx int -} - -func (m *TestMcuWithEvents) UpdateBandwidth(handle uint64, media string, sent api.Bandwidth, received api.Bandwidth) { - assert := assert.New(m.t) - - m.mu.Lock() - defer m.mu.Unlock() - - m.idx++ - switch m.idx { - case 1: - assert.EqualValues(1, handle) - assert.Equal("audio", media) - assert.Equal(api.BandwidthFromBytes(100), sent) - assert.Equal(api.BandwidthFromBytes(200), received) - case 2: - assert.EqualValues(1, handle) - assert.Equal("video", media) - assert.Equal(api.BandwidthFromBytes(200), sent) - assert.Equal(api.BandwidthFromBytes(300), received) - default: - assert.Fail("too many updates", "received update %d (handle=%d, media=%s, sent=%d, received=%d)", m.idx, handle, media, sent, received) - } -} - -func (m *TestMcuWithEvents) WaitForUpdates(ctx context.Context, waitForIdx int) error { - for { - if err := ctx.Err(); err != nil { - return err - } - - m.mu.Lock() - idx := m.idx - m.mu.Unlock() - if idx == waitForIdx { - return nil - } - - time.Sleep(time.Millisecond) - } -} - -type janusEventSender struct { - events []janusEvent -} - -func (s *janusEventSender) SendSingle(t *testing.T, conn *websocket.Conn) { - t.Helper() - - require := require.New(t) - require.Len(s.events, 1) - - require.NoError(conn.WriteJSON(s.events[0])) - s.events = nil -} - -func (s *janusEventSender) Send(t *testing.T, conn *websocket.Conn) { - t.Helper() - - require := require.New(t) - require.NoError(conn.WriteJSON(s.events)) - s.events = nil -} - -func (s *janusEventSender) AddEvent(t *testing.T, eventType int, eventSubtype int, handleId uint64, event any) { - t.Helper() - - require := require.New(t) - assert := assert.New(t) - data, err := json.Marshal(event) - require.NoError(err) - if s, ok := event.(fmt.Stringer); assert.True(ok, "%T should implement fmt.Stringer", event) { - assert.Equal(s.String(), string(data)) - } - - message := janusEvent{ - Type: eventType, - SubType: eventSubtype, - HandleId: handleId, - Event: data, - } - - s.events = append(s.events, message) -} - -func TestJanusEventsHandlerDifferentTypes(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - _, url, handler := NewTestJanusEventsHandlerServer(t) - - mcu := &TestMcuWithEvents{ - t: t, - } - handler.mcu = mcu - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - dialer := websocket.Dialer{ - Subprotocols: []string{ - EventsSubprotocol, - }, - } - conn, response, err := dialer.DialContext(ctx, url, nil) - require.NoError(err) - defer func() { - assert.NoError(conn.Close()) - }() - - assert.Equal(EventsSubprotocol, response.Header.Get("Sec-WebSocket-Protocol")) - - var sender janusEventSender - - sender.AddEvent( - t, - janusEventTypeSession, - 0, - 1, - janusEventSession{ - Name: "created", - }, - ) - - sender.AddEvent( - t, - janusEventTypeHandle, - 0, - 1, - janusEventHandle{ - Name: "attached", - }, - ) - - sender.AddEvent( - t, - janusEventTypeExternal, - 0, - 0, - janusEventExternal{ - Schema: "test-external", - }, - ) - - sender.AddEvent( - t, - janusEventTypeJSEP, - 0, - 1, - janusEventJSEP{ - Owner: "testing", - }, - ) - - sender.AddEvent( - t, - janusEventTypeWebRTC, - janusEventSubTypeWebRTCICE, - 1, - janusEventWebRTCICE{ - ICE: "gathering", - }, - ) - - sender.AddEvent( - t, - janusEventTypeWebRTC, - janusEventSubTypeWebRTCLocalCandidate, - 1, - janusEventWebRTCLocalCandidate{ - LocalCandidate: "invalid-candidate", - }, - ) - - sender.AddEvent( - t, - janusEventTypeWebRTC, - janusEventSubTypeWebRTCRemoteCandidate, - 1, - janusEventWebRTCRemoteCandidate{ - RemoteCandidate: "invalid-candidate", - }, - ) - - sender.AddEvent( - t, - janusEventTypeWebRTC, - janusEventSubTypeWebRTCSelectedPair, - 1, - janusEventWebRTCSelectedPair{ - SelectedPair: "invalid-pair", - }, - ) - - sender.AddEvent( - t, - janusEventTypeWebRTC, - janusEventSubTypeWebRTCDTLS, - 1, - janusEventWebRTCDTLS{ - DTLS: "trying", - }, - ) - - sender.AddEvent( - t, - janusEventTypeWebRTC, - janusEventSubTypeWebRTCPeerConnection, - 1, - janusEventWebRTCPeerConnection{ - Connection: "webrtcup", - }, - ) - - sender.AddEvent( - t, - janusEventTypeMedia, - janusEventSubTypeMediaState, - 1, - janusEventMediaState{ - Media: "audio", - }, - ) - - sender.AddEvent( - t, - janusEventTypeMedia, - janusEventSubTypeMediaSlowLink, - 1, - janusEventMediaSlowLink{ - Media: "audio", - }, - ) - - sender.AddEvent( - t, - janusEventTypePlugin, - 0, - 1, - janusEventPlugin{ - Plugin: "test-plugin", - }, - ) - - sender.AddEvent( - t, - janusEventTypeTransport, - 0, - 1, - janusEventTransport{ - Transport: "test-transport", - }, - ) - - sender.AddEvent( - t, - janusEventTypeCore, - janusEventSubTypeCoreStatusStartup, - 0, - janusEventCoreStartup{ - Status: "started", - }, - ) - - sender.AddEvent( - t, - janusEventTypeCore, - janusEventSubTypeCoreStatusStartup, - 0, - janusEventCoreStartup{ - Status: "update", - }, - ) - - sender.AddEvent( - t, - janusEventTypeCore, - janusEventSubTypeCoreStatusShutdown, - 0, - janusEventCoreShutdown{ - Status: "shutdown", - }, - ) - - sender.AddEvent( - t, - janusEventTypeMedia, - janusEventSubTypeMediaStats, - 1, - janusEventMediaStats{ - Media: "audio", - BytesSentLastSec: 100, - BytesReceivedLastSec: 200, - }, - ) - sender.Send(t, conn) - - // Wait until all events are processed. - assert.NoError(mcu.WaitForUpdates(ctx, 1)) -} - -func TestJanusEventsHandlerNotGrouped(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - _, url, handler := NewTestJanusEventsHandlerServer(t) - - mcu := &TestMcuWithEvents{ - t: t, - } - handler.mcu = mcu - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - dialer := websocket.Dialer{ - Subprotocols: []string{ - EventsSubprotocol, - }, - } - conn, response, err := dialer.DialContext(ctx, url, nil) - require.NoError(err) - defer func() { - assert.NoError(conn.Close()) - }() - - assert.Equal(EventsSubprotocol, response.Header.Get("Sec-WebSocket-Protocol")) - - metricstest.AssertCollectorChangeBy(t, statsJanusMediaNACKTotal.WithLabelValues("audio", "incoming"), 20) - metricstest.AssertCollectorChangeBy(t, statsJanusMediaNACKTotal.WithLabelValues("audio", "outgoing"), 30) - metricstest.AssertCollectorChangeBy(t, statsJanusMediaRetransmissionsTotal.WithLabelValues("audio"), 40) - metricstest.AssertCollectorChangeBy(t, statsJanusMediaLostTotal.WithLabelValues("audio", "local"), 50) - metricstest.AssertCollectorChangeBy(t, statsJanusMediaLostTotal.WithLabelValues("audio", "remote"), 60) - - var sender janusEventSender - sender.AddEvent( - t, - janusEventTypeHandle, - 0, - 1, - janusEventHandle{ - Name: "attached", - }, - ) - sender.SendSingle(t, conn) - sender.AddEvent( - t, - janusEventTypeMedia, - janusEventSubTypeMediaStats, - 1, - janusEventMediaStats{ - Media: "audio", - BytesSentLastSec: 100, - BytesReceivedLastSec: 200, - Codec: "opus", - RTT: 10, - JitterLocal: 11, - JitterRemote: 12, - NacksReceived: 20, - NacksSent: 30, - RetransmissionsReceived: 40, - Lost: 50, - LostByRemote: 60, - }, - ) - sender.SendSingle(t, conn) - sender.AddEvent( - t, - janusEventTypeHandle, - 0, - 1, - janusEventHandle{ - Name: "detached", - }, - ) - sender.SendSingle(t, conn) - assert.NoError(mcu.WaitForUpdates(ctx, 1)) -} - -func TestJanusEventsHandlerGrouped(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - _, url, handler := NewTestJanusEventsHandlerServer(t) - - mcu := &TestMcuWithEvents{ - t: t, - } - handler.mcu = mcu - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - dialer := websocket.Dialer{ - Subprotocols: []string{ - EventsSubprotocol, - }, - } - conn, response, err := dialer.DialContext(ctx, url, nil) - require.NoError(err) - defer func() { - assert.NoError(conn.Close()) - }() - - assert.Equal(EventsSubprotocol, response.Header.Get("Sec-WebSocket-Protocol")) - - var sender janusEventSender - sender.AddEvent( - t, - janusEventTypeMedia, - janusEventSubTypeMediaStats, - 1, - janusEventMediaStats{ - Media: "audio", - BytesSentLastSec: 100, - BytesReceivedLastSec: 200, - }, - ) - sender.AddEvent( - t, - janusEventTypeMedia, - janusEventSubTypeMediaStats, - 1, - janusEventMediaStats{ - Media: "video", - BytesSentLastSec: 200, - BytesReceivedLastSec: 300, - }, - ) - sender.Send(t, conn) - - assert.NoError(mcu.WaitForUpdates(ctx, 2)) -} - -func TestValueCounter(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - - var c valueCounter - assert.EqualValues(0, c.Update("foo", 0)) - assert.EqualValues(10, c.Update("foo", 10)) - assert.EqualValues(0, c.Update("foo", 10)) - assert.EqualValues(1, c.Update("foo", 11)) - assert.EqualValues(10, c.Update("bar", 10)) - assert.EqualValues(1, c.Update("bar", 11)) - assert.Equal(uint64(math.MaxUint64-10), c.Update("baz", math.MaxUint64-10)) - assert.EqualValues(20, c.Update("baz", 10)) -} diff --git a/sfu/janus/janus.go b/sfu/janus/janus.go deleted file mode 100644 index 9efbd9b..0000000 --- a/sfu/janus/janus.go +++ /dev/null @@ -1,1181 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strconv" - "sync" - "sync/atomic" - "time" - - "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async" - "github.com/strukturag/nextcloud-spreed-signaling/v2/container" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - sfuinternal "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -const ( - pluginVideoRoom = "janus.plugin.videoroom" - eventWebsocket = "janus.eventhandler.wsevh" - - keepaliveInterval = 30 * time.Second - bandwidthInterval = time.Second - - videoPublisherUserId = 1 - screenPublisherUserId = 2 - - initialReconnectInterval = 1 * time.Second - maxReconnectInterval = 16 * time.Second - - // MCU requests will be cancelled if they take too long. - defaultMcuTimeoutSeconds = 10 -) - -var ( - ErrRemoteStreamsNotSupported = errors.New("need Janus 1.1.0 for remote streams") - - streamTypeUserIds = map[sfu.StreamType]uint64{ - sfu.StreamTypeVideo: videoPublisherUserId, - sfu.StreamTypeScreen: screenPublisherUserId, - } -) - -func getPluginValue(data janus.PluginData, pluginName string, key string) any { - if data.Plugin != pluginName { - return nil - } - - return data.Data[key] -} - -func convertIntValue(value any) (uint64, error) { - switch t := value.(type) { - case float64: - if t < 0 { - return 0, fmt.Errorf("unsupported float64 number: %+v", t) - } - return uint64(t), nil - case uint64: - return t, nil - case int: - if t < 0 { - return 0, fmt.Errorf("unsupported int number: %+v", t) - } - return uint64(t), nil - case int64: - if t < 0 { - return 0, fmt.Errorf("unsupported int64 number: %+v", t) - } - return uint64(t), nil - case json.Number: - r, err := t.Int64() - if err != nil { - return 0, err - } else if r < 0 { - return 0, fmt.Errorf("unsupported JSON number: %+v", t) - } - return uint64(r), nil - default: - return 0, fmt.Errorf("unknown number type: %+v (%T)", t, t) - } -} - -func getPluginIntValue(logger log.Logger, data janus.PluginData, pluginName string, key string) uint64 { - val := getPluginValue(data, pluginName, key) - if val == nil { - return 0 - } - - result, err := convertIntValue(val) - if err != nil { - logger.Printf("Invalid value %+v for %s: %s", val, key, err) - result = 0 - } - return result -} - -func getPluginStringValue(data janus.PluginData, pluginName string, key string) string { - val := getPluginValue(data, pluginName, key) - if val == nil { - return "" - } - - strVal, ok := val.(string) - if !ok { - return "" - } - - return strVal -} - -// TODO(jojo): Lots of error handling still missing. - -type clientInterface interface { - Handle() uint64 - - NotifyReconnected() - - Bandwidth() *sfu.ClientBandwidthInfo - UpdateBandwidth(media string, sent api.Bandwidth, received api.Bandwidth) -} - -type Settings struct { - sfuinternal.CommonSettings - - allowedCandidates atomic.Pointer[container.IPList] - blockedCandidates atomic.Pointer[container.IPList] -} - -func newJanusSettings(ctx context.Context, config *goconf.ConfigFile) (*Settings, error) { - settings := &Settings{ - CommonSettings: sfuinternal.CommonSettings{ - Logger: log.LoggerFromContext(ctx), - }, - } - if err := settings.load(config); err != nil { - return nil, err - } - - return settings, nil -} - -func (s *Settings) load(config *goconf.ConfigFile) error { - if err := s.Load(config); err != nil { - return err - } - - mcuTimeoutSeconds, _ := config.GetInt("mcu", "timeout") - if mcuTimeoutSeconds <= 0 { - mcuTimeoutSeconds = defaultMcuTimeoutSeconds - } - mcuTimeout := time.Duration(mcuTimeoutSeconds) * time.Second - s.Logger.Printf("Using a timeout of %s for MCU requests", mcuTimeout) - s.SetTimeout(mcuTimeout) - - if value, _ := config.GetString("mcu", "allowedcandidates"); value != "" { - allowed, err := container.ParseIPList(value) - if err != nil { - return fmt.Errorf("invalid allowedcandidates: %w", err) - } - - s.Logger.Printf("Candidates allowlist: %s", allowed) - s.allowedCandidates.Store(allowed) - } else { - s.Logger.Printf("No candidates allowlist") - s.allowedCandidates.Store(nil) - } - if value, _ := config.GetString("mcu", "blockedcandidates"); value != "" { - blocked, err := container.ParseIPList(value) - if err != nil { - return fmt.Errorf("invalid blockedcandidates: %w", err) - } - - s.Logger.Printf("Candidates blocklist: %s", blocked) - s.blockedCandidates.Store(blocked) - } else { - s.Logger.Printf("No candidates blocklist") - s.blockedCandidates.Store(nil) - } - - return nil -} - -func (s *Settings) Reload(config *goconf.ConfigFile) { - if err := s.load(config); err != nil { - s.Logger.Printf("Error reloading MCU settings: %s", err) - } -} - -type Stats interface { - IncSubscriber(streamType sfu.StreamType) - DecSubscriber(streamType sfu.StreamType) -} - -type prometheusJanusStats struct{} - -func (s *prometheusJanusStats) IncSubscriber(streamType sfu.StreamType) { - sfuinternal.StatsSubscribersCurrent.WithLabelValues(string(streamType)).Inc() -} - -func (s *prometheusJanusStats) DecSubscriber(streamType sfu.StreamType) { - sfuinternal.StatsSubscribersCurrent.WithLabelValues(string(streamType)).Dec() -} - -type refcountCond struct { - ref int - cond *sync.Cond -} - -type janusSFU struct { - logger log.Logger - - url string - mu sync.Mutex - - settings *Settings - stats Stats - - createJanusGateway func(ctx context.Context, wsURL string, listener janus.GatewayListener) (janus.GatewayInterface, error) - - gw janus.GatewayInterface - session *janus.Session - handle *janus.Handle - - version int - info atomic.Pointer[janus.InfoMsg] - - closeChan chan struct{} - - muClients sync.RWMutex - // +checklocks:muClients - clients map[uint64]clientInterface - clientId atomic.Uint64 - - // +checklocks:mu - publishers map[sfu.StreamId]*janusPublisher - // +checklocks:mu - publisherCreated map[sfu.StreamId]*refcountCond - publisherConnected async.Notifier - // +checklocks:mu - remotePublishers map[sfu.StreamId]*janusRemotePublisher - - reconnectTimer *time.Timer - reconnectInterval time.Duration - - connectedSince time.Time - onConnected atomic.Value - onDisconnected atomic.Value -} - -func emptyOnConnected() {} -func emptyOnDisconnected() {} - -func NewJanusSFU(ctx context.Context, url string, config *goconf.ConfigFile) (sfu.SFU, error) { - settings, err := newJanusSettings(ctx, config) - if err != nil { - return nil, err - } - - mcu := &janusSFU{ - logger: log.LoggerFromContext(ctx), - url: url, - settings: settings, - stats: &prometheusJanusStats{}, - closeChan: make(chan struct{}, 1), - clients: make(map[uint64]clientInterface), - - publishers: make(map[sfu.StreamId]*janusPublisher), - publisherCreated: make(map[sfu.StreamId]*refcountCond), - remotePublishers: make(map[sfu.StreamId]*janusRemotePublisher), - - createJanusGateway: func(ctx context.Context, wsURL string, listener janus.GatewayListener) (janus.GatewayInterface, error) { - return janus.NewGateway(ctx, wsURL, listener) - }, - reconnectInterval: initialReconnectInterval, - } - mcu.onConnected.Store(emptyOnConnected) - mcu.onDisconnected.Store(emptyOnDisconnected) - - mcu.reconnectTimer = time.AfterFunc(mcu.reconnectInterval, func() { - mcu.doReconnect(context.Background()) - }) - mcu.reconnectTimer.Stop() - if mcu.url != "" { - if err := mcu.reconnect(ctx); err != nil { - return nil, err - } - } - return mcu, nil -} - -func NewJanusSFUWithGateway(ctx context.Context, gateway janus.GatewayInterface, config *goconf.ConfigFile) (sfu.SFU, error) { - sfu, err := NewJanusSFU(ctx, "", config) - if err != nil { - return nil, err - } - - sfuJanus := sfu.(*janusSFU) - sfuJanus.createJanusGateway = func(ctx context.Context, wsURL string, listener janus.GatewayListener) (janus.GatewayInterface, error) { - return gateway, nil - } - return sfu, nil -} - -func (m *janusSFU) SetStats(stats Stats) { - m.stats = stats -} - -func (m *janusSFU) Settings() *Settings { - return m.settings -} - -func (m *janusSFU) disconnect() { - if handle := m.handle; handle != nil { - m.handle = nil - m.closeChan <- struct{}{} - if _, err := handle.Detach(context.TODO()); err != nil { - m.logger.Printf("Error detaching handle %d: %s", handle.Id, err) - } - } - if m.session != nil { - if _, err := m.session.Destroy(context.TODO()); err != nil { - m.logger.Printf("Error destroying session %d: %s", m.session.Id, err) - } - m.session = nil - } - if m.gw != nil { - if err := m.gw.Close(); err != nil { - m.logger.Println("Error while closing connection to MCU", err) - } - m.gw = nil - } -} - -func (m *janusSFU) GetBandwidthLimits() (api.Bandwidth, api.Bandwidth) { - return m.settings.MaxStreamBitrate(), m.settings.MaxScreenBitrate() -} - -func (m *janusSFU) Bandwidth() (result *sfu.ClientBandwidthInfo) { - m.muClients.RLock() - defer m.muClients.RUnlock() - - for _, client := range m.clients { - if bandwidth := client.Bandwidth(); bandwidth != nil { - if result == nil { - result = &sfu.ClientBandwidthInfo{} - } - result.Received += bandwidth.Received - result.Sent += bandwidth.Sent - } - } - return -} - -type janusBandwidthStats interface { - SetBandwidth(incoming uint64, outgoing uint64) -} - -type prometheusJanusBandwidthStats struct{} - -func (s *prometheusJanusBandwidthStats) SetBandwidth(incoming uint64, outgoing uint64) { - statsJanusBandwidthCurrent.WithLabelValues("incoming").Set(float64(incoming)) - statsJanusBandwidthCurrent.WithLabelValues("outgoing").Set(float64(outgoing)) -} - -var ( - defaultJanusBandwidthStats = &prometheusJanusBandwidthStats{} -) - -func (m *janusSFU) updateBandwidthStats(stats janusBandwidthStats) { - if info := m.info.Load(); info != nil { - if !info.EventHandlers { - // Event handlers are disabled, no stats will be available. - return - } - - if _, found := info.Events[eventWebsocket]; !found { - // Event handler plugin not found, no stats will be available. - return - } - } - - if stats == nil { - stats = defaultJanusBandwidthStats - } - - if bandwidth := m.Bandwidth(); bandwidth != nil { - stats.SetBandwidth(bandwidth.Received.Bytes(), bandwidth.Sent.Bytes()) - } else { - stats.SetBandwidth(0, 0) - } -} - -func (m *janusSFU) reconnect(ctx context.Context) error { - m.disconnect() - gw, err := m.createJanusGateway(ctx, m.url, m) - if err != nil { - return err - } - - m.gw = gw - m.reconnectTimer.Stop() - return nil -} - -func (m *janusSFU) doReconnect(ctx context.Context) { - if err := m.reconnect(ctx); err != nil { - m.scheduleReconnect(err) - return - } - if err := m.Start(ctx); err != nil { - m.scheduleReconnect(err) - return - } - - m.logger.Println("Reconnection to Janus gateway successful") - m.mu.Lock() - clear(m.publishers) - for _, c := range m.publisherCreated { - c.cond.Broadcast() - } - clear(m.publisherCreated) - m.publisherConnected.Reset() - m.reconnectInterval = initialReconnectInterval - m.mu.Unlock() - - m.notifyClientsReconnected() -} - -func (m *janusSFU) notifyClientsReconnected() { - m.muClients.RLock() - defer m.muClients.RUnlock() - - for oldHandle, client := range m.clients { - go func(oldHandle uint64, client clientInterface) { - client.NotifyReconnected() - newHandle := client.Handle() - - if oldHandle != newHandle { - m.muClients.Lock() - defer m.muClients.Unlock() - - delete(m.clients, oldHandle) - m.clients[newHandle] = client - } - }(oldHandle, client) - } -} - -func (m *janusSFU) scheduleReconnect(err error) { - m.mu.Lock() - defer m.mu.Unlock() - m.reconnectTimer.Reset(m.reconnectInterval) - if err == nil { - m.logger.Printf("Connection to Janus gateway was interrupted, reconnecting in %s", m.reconnectInterval) - } else { - m.logger.Printf("Reconnect to Janus gateway failed (%s), reconnecting in %s", err, m.reconnectInterval) - } - - m.reconnectInterval = min(m.reconnectInterval*2, maxReconnectInterval) -} - -func (m *janusSFU) ConnectionInterrupted() { - m.scheduleReconnect(nil) - m.notifyOnDisconnected() -} - -func (m *janusSFU) isMultistream() bool { - return m.version >= 1000 -} - -func (m *janusSFU) hasRemotePublisher() bool { - return m.version >= 1100 -} - -func (m *janusSFU) Start(ctx context.Context) error { - if m.url == "" { - if err := m.reconnect(ctx); err != nil { - return err - } - } - info, err := m.gw.Info(ctx) - if err != nil { - return err - } - - m.logger.Printf("Connected to %s %s by %s", info.Name, info.VersionString, info.Author) - m.version = info.Version - - if plugin, found := info.Plugins[pluginVideoRoom]; found { - m.logger.Printf("Found %s %s by %s", plugin.Name, plugin.VersionString, plugin.Author) - } else { - return fmt.Errorf("plugin %s is not supported", pluginVideoRoom) - } - - if plugin, found := info.Events[eventWebsocket]; found { - if !info.EventHandlers { - m.logger.Printf("Found %s %s by %s but event handlers are disabled, realtime usage will not be available", plugin.Name, plugin.VersionString, plugin.Author) - } else { - m.logger.Printf("Found %s %s by %s", plugin.Name, plugin.VersionString, plugin.Author) - } - } else { - m.logger.Printf("Plugin %s not found, realtime usage will not be available", eventWebsocket) - } - - m.logger.Printf("Used dependencies: %+v", info.Dependencies) - if !info.DataChannels { - return errors.New("data channels are not supported") - } - - m.logger.Println("Data channels are supported") - if !info.FullTrickle { - m.logger.Println("WARNING: Full-Trickle is NOT enabled in Janus!") - } else { - m.logger.Println("Full-Trickle is enabled") - } - - if m.session, err = m.gw.Create(ctx); err != nil { - m.disconnect() - return err - } - m.logger.Println("Created Janus session", m.session.Id) - m.connectedSince = time.Now() - - if m.handle, err = m.session.Attach(ctx, pluginVideoRoom); err != nil { - m.disconnect() - return err - } - m.logger.Println("Created Janus handle", m.handle.Id) - - m.info.Store(info) - - go m.run() - - m.notifyOnConnected() - return nil -} - -func (m *janusSFU) registerClient(client clientInterface) { - m.muClients.Lock() - defer m.muClients.Unlock() - - m.clients[client.Handle()] = client -} - -func (m *janusSFU) unregisterClient(client clientInterface) { - m.muClients.Lock() - defer m.muClients.Unlock() - - delete(m.clients, client.Handle()) -} - -func (m *janusSFU) run() { - ticker := time.NewTicker(keepaliveInterval) - defer ticker.Stop() - - bandwidthTicker := time.NewTicker(bandwidthInterval) - defer bandwidthTicker.Stop() - -loop: - for { - select { - case <-ticker.C: - m.sendKeepalive(context.Background()) - case <-bandwidthTicker.C: - m.updateBandwidthStats(nil) - case <-m.closeChan: - break loop - } - } -} - -func (m *janusSFU) Stop() { - m.disconnect() - m.reconnectTimer.Stop() -} - -func (m *janusSFU) IsConnected() bool { - return m.handle != nil -} - -func (m *janusSFU) Info() *janus.InfoMsg { - return m.info.Load() -} - -func (m *janusSFU) GetServerInfoSfu() *talk.BackendServerInfoSfu { - janus := &talk.BackendServerInfoSfuJanus{ - Url: m.url, - } - if m.IsConnected() { - janus.Connected = true - if info := m.Info(); info != nil { - janus.Name = info.Name - janus.Version = info.VersionString - janus.Author = info.Author - janus.DataChannels = internal.MakePtr(info.DataChannels) - janus.FullTrickle = internal.MakePtr(info.FullTrickle) - janus.LocalIP = info.LocalIP - janus.IPv6 = internal.MakePtr(info.IPv6) - - if plugin, found := info.Plugins[pluginVideoRoom]; found { - janus.VideoRoom = &talk.BackendServerInfoVideoRoom{ - Name: plugin.Name, - Version: plugin.VersionString, - Author: plugin.Author, - } - } - } - } - - sfu := &talk.BackendServerInfoSfu{ - Mode: talk.SfuModeJanus, - Janus: janus, - } - return sfu -} - -func (m *janusSFU) Reload(config *goconf.ConfigFile) { - m.settings.Reload(config) -} - -func (m *janusSFU) SetOnConnected(f func()) { - if f == nil { - f = emptyOnConnected - } - - m.onConnected.Store(f) -} - -func (m *janusSFU) notifyOnConnected() { - f := m.onConnected.Load().(func()) - f() -} - -func (m *janusSFU) SetOnDisconnected(f func()) { - if f == nil { - f = emptyOnDisconnected - } - - m.onDisconnected.Store(f) -} - -func (m *janusSFU) notifyOnDisconnected() { - f := m.onDisconnected.Load().(func()) - f() -} - -type janusConnectionStats struct { - Url string `json:"url"` - Connected bool `json:"connected"` - Publishers int64 `json:"publishers"` - Clients int64 `json:"clients"` - Uptime *time.Time `json:"uptime,omitempty"` -} - -func (m *janusSFU) GetStats() any { - result := janusConnectionStats{ - Url: m.url, - } - if m.session != nil { - result.Connected = true - result.Uptime = &m.connectedSince - } - m.mu.Lock() - result.Publishers = int64(len(m.publishers)) - m.mu.Unlock() - m.muClients.Lock() - result.Clients = int64(len(m.clients)) - m.muClients.Unlock() - return result -} - -func (m *janusSFU) sendKeepalive(ctx context.Context) { - if _, err := m.session.KeepAlive(ctx); err != nil { - m.logger.Println("Could not send keepalive request", err) - if e, ok := err.(*janus.ErrorMsg); ok { - switch e.Err.Code { - case janus.JANUS_ERROR_SESSION_NOT_FOUND: - m.scheduleReconnect(err) - } - } - } -} - -func (m *janusSFU) SubscriberConnected(id string, publisher api.PublicSessionId, streamType sfu.StreamType) { - m.mu.Lock() - defer m.mu.Unlock() - - if p, found := m.publishers[sfu.GetStreamId(publisher, streamType)]; found { - p.stats.AddSubscriber(id) - } -} - -func (m *janusSFU) SubscriberDisconnected(id string, publisher api.PublicSessionId, streamType sfu.StreamType) { - m.mu.Lock() - defer m.mu.Unlock() - - if p, found := m.publishers[sfu.GetStreamId(publisher, streamType)]; found { - p.stats.RemoveSubscriber(id) - } -} - -func (m *janusSFU) createPublisherRoom(ctx context.Context, handle *janus.Handle, id api.PublicSessionId, streamType sfu.StreamType, settings sfu.NewPublisherSettings) (uint64, api.Bandwidth, error) { - create_msg := api.StringMap{ - "request": "create", - "description": sfu.GetStreamId(id, streamType), - // We publish every stream in its own Janus room. - "publishers": 1, - // Do not use the video-orientation RTP extension as it breaks video - // orientation changes in Firefox. - "videoorient_ext": false, - } - if codec := settings.AudioCodec; codec != "" { - create_msg["audiocodec"] = codec - } - if codec := settings.VideoCodec; codec != "" { - create_msg["videocodec"] = codec - } - if profile := settings.VP9Profile; profile != "" { - create_msg["vp9_profile"] = profile - } - if profile := settings.H264Profile; profile != "" { - create_msg["h264_profile"] = profile - } - var maxBitrate api.Bandwidth - if streamType == sfu.StreamTypeScreen { - maxBitrate = m.settings.MaxScreenBitrate() - } else { - maxBitrate = m.settings.MaxStreamBitrate() - } - bitrate := settings.Bitrate - if bitrate <= 0 { - bitrate = maxBitrate - } else { - bitrate = min(bitrate, maxBitrate) - } - create_msg["bitrate"] = bitrate.Bits() - create_response, err := handle.Request(ctx, create_msg) - if err != nil { - if _, err2 := handle.Detach(ctx); err2 != nil { - m.logger.Printf("Error detaching handle %d: %s", handle.Id, err2) - } - return 0, 0, err - } - - roomId := getPluginIntValue(m.logger, create_response.PluginData, pluginVideoRoom, "room") - if roomId == 0 { - if _, err := handle.Detach(ctx); err != nil { - m.logger.Printf("Error detaching handle %d: %s", handle.Id, err) - } - return 0, 0, fmt.Errorf("no room id received: %+v", create_response) - } - - m.logger.Println("Created room", roomId, create_response.PluginData) - return roomId, bitrate, nil -} - -func (m *janusSFU) getOrCreatePublisherHandle(ctx context.Context, id api.PublicSessionId, streamType sfu.StreamType, settings sfu.NewPublisherSettings) (*janus.Handle, uint64, uint64, api.Bandwidth, error) { - session := m.session - if session == nil { - return nil, 0, 0, 0, sfu.ErrNotConnected - } - handle, err := session.Attach(ctx, pluginVideoRoom) - if err != nil { - return nil, 0, 0, 0, err - } - - m.logger.Printf("Attached %s as publisher %d to plugin %s in session %d", streamType, handle.Id, pluginVideoRoom, session.Id) - - roomId, bitrate, err := m.createPublisherRoom(ctx, handle, id, streamType, settings) - if err != nil { - if _, err2 := handle.Detach(ctx); err2 != nil { - m.logger.Printf("Error detaching handle %d: %s", handle.Id, err2) - } - return nil, 0, 0, 0, err - } - - msg := api.StringMap{ - "request": "join", - "ptype": "publisher", - "room": roomId, - "id": streamTypeUserIds[streamType], - } - - response, err := handle.Message(ctx, msg, nil) - if err != nil { - if _, err2 := handle.Detach(ctx); err2 != nil { - m.logger.Printf("Error detaching handle %d: %s", handle.Id, err2) - } - return nil, 0, 0, 0, err - } - - return handle, response.Session, roomId, bitrate, nil -} - -func (m *janusSFU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { - if _, found := streamTypeUserIds[streamType]; !found { - return nil, fmt.Errorf("unsupported stream type %s", streamType) - } - - handle, session, roomId, maxBitrate, err := m.getOrCreatePublisherHandle(ctx, id, streamType, settings) - if err != nil { - return nil, err - } - - client := &janusPublisher{ - janusClient: janusClient{ - logger: m.logger, - mcu: m, - listener: listener, - - id: m.clientId.Add(1), - session: session, - roomId: roomId, - sid: sid, - streamType: streamType, - maxBitrate: maxBitrate, - - closeChan: make(chan struct{}, 1), - deferred: make(chan func(), 64), - }, - sdpReady: internal.NewCloser(), - id: id, - settings: settings, - } - client.handle.Store(handle) - client.handleId.Store(handle.Id) - client.janusClient.handleEvent = client.handleEvent - client.janusClient.handleHangup = client.handleHangup - client.janusClient.handleDetached = client.handleDetached - client.janusClient.handleConnected = client.handleConnected - client.janusClient.handleSlowLink = client.handleSlowLink - client.janusClient.handleMedia = client.handleMedia - - m.registerClient(client) - m.logger.Printf("Publisher %s is using handle %d", client.id, handle.Id) - go client.run(handle, client.closeChan) - - m.notifyPublisherCreated(id, streamType, client) - sfuinternal.StatsPublishersCurrent.WithLabelValues(string(streamType)).Inc() - sfuinternal.StatsPublishersTotal.WithLabelValues(string(streamType)).Inc() - return client, nil -} - -func (m *janusSFU) notifyPublisherCreated(id api.PublicSessionId, streamType sfu.StreamType, client *janusPublisher) { - key := sfu.GetStreamId(id, streamType) - m.mu.Lock() - defer m.mu.Unlock() - - m.publishers[key] = client - if c, found := m.publisherCreated[key]; found { - c.cond.Broadcast() - delete(m.publisherCreated, key) - } -} - -func (m *janusSFU) notifyPublisherConnected(id api.PublicSessionId, streamType sfu.StreamType) { - key := sfu.GetStreamId(id, streamType) - m.publisherConnected.Notify(string(key)) -} - -func (m *janusSFU) newPublisherConnectedWaiter(id api.PublicSessionId, streamType sfu.StreamType) (*async.Waiter, async.ReleaseFunc) { - key := sfu.GetStreamId(id, streamType) - - return m.publisherConnected.NewWaiter(string(key)) -} - -func (m *janusSFU) getPublisher(ctx context.Context, publisher api.PublicSessionId, streamType sfu.StreamType) (*janusPublisher, error) { - // Do the direct check immediately as this should be the normal case. - key := sfu.GetStreamId(publisher, streamType) - m.mu.Lock() - defer m.mu.Unlock() - result, found := m.publishers[key] - if found { - return result, nil - } - - c, found := m.publisherCreated[key] - if !found { - c = &refcountCond{ - cond: sync.NewCond(&m.mu), - } - m.publisherCreated[key] = c - } - c.ref++ - - stop := context.AfterFunc(ctx, func() { - c.cond.Broadcast() - }) - defer stop() - - for result == nil && ctx.Err() == nil { - c.cond.Wait() - result = m.publishers[key] - } - - c.ref-- - if c.ref == 0 { - delete(m.publisherCreated, key) - } - - if err := ctx.Err(); err != nil { - return nil, err - } - - return result, nil -} - -func (m *janusSFU) getOrCreateSubscriberHandle(ctx context.Context, publisher api.PublicSessionId, streamType sfu.StreamType) (*janus.Handle, *janusPublisher, error) { - var pub *janusPublisher - var err error - if pub, err = m.getPublisher(ctx, publisher, streamType); err != nil { - return nil, nil, err - } - - session := m.session - if session == nil { - return nil, nil, sfu.ErrNotConnected - } - - handle, err := session.Attach(ctx, pluginVideoRoom) - if err != nil { - return nil, nil, err - } - - m.logger.Printf("Attached subscriber to room %d of publisher %s in plugin %s in session %d as %d", pub.roomId, publisher, pluginVideoRoom, session.Id, handle.Id) - return handle, pub, nil -} - -func (m *janusSFU) NewSubscriber(ctx context.Context, listener sfu.Listener, publisher api.PublicSessionId, streamType sfu.StreamType, initiator sfu.Initiator) (sfu.Subscriber, error) { - if _, found := streamTypeUserIds[streamType]; !found { - return nil, fmt.Errorf("unsupported stream type %s", streamType) - } - - handle, pub, err := m.getOrCreateSubscriberHandle(ctx, publisher, streamType) - if err != nil { - return nil, err - } - - client := &janusSubscriber{ - janusClient: janusClient{ - logger: m.logger, - mcu: m, - listener: listener, - - id: m.clientId.Add(1), - roomId: pub.roomId, - sid: strconv.FormatUint(handle.Id, 10), - streamType: streamType, - maxBitrate: pub.MaxBitrate(), - - closeChan: make(chan struct{}, 1), - deferred: make(chan func(), 64), - }, - publisher: publisher, - } - client.handle.Store(handle) - client.handleId.Store(handle.Id) - client.janusClient.handleEvent = client.handleEvent - client.janusClient.handleHangup = client.handleHangup - client.janusClient.handleDetached = client.handleDetached - client.janusClient.handleConnected = client.handleConnected - client.janusClient.handleSlowLink = client.handleSlowLink - client.janusClient.handleMedia = client.handleMedia - m.registerClient(client) - go client.run(handle, client.closeChan) - m.stats.IncSubscriber(streamType) - sfuinternal.StatsSubscribersTotal.WithLabelValues(string(streamType)).Inc() - return client, nil -} - -func (m *janusSFU) getOrCreateRemotePublisher(ctx context.Context, controller sfu.RemotePublisherController, streamType sfu.StreamType, settings sfu.NewPublisherSettings) (*janusRemotePublisher, error) { - m.mu.Lock() - defer m.mu.Unlock() - pub, found := m.remotePublishers[sfu.GetStreamId(controller.PublisherId(), streamType)] - if found { - return pub, nil - } - - streams, err := controller.GetStreams(ctx) - if err != nil { - return nil, err - } - - if len(streams) == 0 { - return nil, errors.New("remote publisher has no streams") - } - - session := m.session - if session == nil { - return nil, sfu.ErrNotConnected - } - - handle, err := session.Attach(ctx, pluginVideoRoom) - if err != nil { - return nil, err - } - - roomId, maxBitrate, err := m.createPublisherRoom(ctx, handle, controller.PublisherId(), streamType, settings) - if err != nil { - if _, err2 := handle.Detach(ctx); err2 != nil { - m.logger.Printf("Error detaching handle %d: %s", handle.Id, err2) - } - return nil, err - } - - response, err := handle.Request(ctx, api.StringMap{ - "request": "add_remote_publisher", - "room": roomId, - "id": streamTypeUserIds[streamType], - "streams": streams, - }) - if err != nil { - if _, err2 := handle.Detach(ctx); err2 != nil { - m.logger.Printf("Error detaching handle %d: %s", handle.Id, err2) - } - return nil, err - } - - id := getPluginIntValue(m.logger, response.PluginData, pluginVideoRoom, "id") - port := getPluginIntValue(m.logger, response.PluginData, pluginVideoRoom, "port") - rtcp_port := getPluginIntValue(m.logger, response.PluginData, pluginVideoRoom, "rtcp_port") - - pub = &janusRemotePublisher{ - janusPublisher: janusPublisher{ - janusClient: janusClient{ - logger: m.logger, - mcu: m, - - id: id, - session: response.Session, - roomId: roomId, - sid: strconv.FormatUint(handle.Id, 10), - streamType: streamType, - maxBitrate: maxBitrate, - - closeChan: make(chan struct{}, 1), - deferred: make(chan func(), 64), - }, - - sdpReady: internal.NewCloser(), - id: controller.PublisherId(), - settings: settings, - }, - - controller: controller, - - port: int(port), - rtcpPort: int(rtcp_port), - } - pub.handle.Store(handle) - pub.handleId.Store(handle.Id) - pub.janusClient.handleEvent = pub.handleEvent - pub.janusClient.handleHangup = pub.handleHangup - pub.janusClient.handleDetached = pub.handleDetached - pub.janusClient.handleConnected = pub.handleConnected - pub.janusClient.handleSlowLink = pub.handleSlowLink - pub.janusClient.handleMedia = pub.handleMedia - - if err := controller.StartPublishing(ctx, pub); err != nil { - go pub.Close(context.Background()) - return nil, err - } - - m.remotePublishers[sfu.GetStreamId(controller.PublisherId(), streamType)] = pub - - return pub, nil -} - -func (m *janusSFU) NewRemotePublisher(ctx context.Context, listener sfu.Listener, controller sfu.RemotePublisherController, streamType sfu.StreamType) (sfu.RemotePublisher, error) { - if _, found := streamTypeUserIds[streamType]; !found { - return nil, fmt.Errorf("unsupported stream type %s", streamType) - } - - if !m.hasRemotePublisher() { - return nil, ErrRemoteStreamsNotSupported - } - - pub, err := m.getOrCreateRemotePublisher(ctx, controller, streamType, sfu.NewPublisherSettings{}) - if err != nil { - return nil, err - } - - pub.addRef() - return pub, nil -} - -func (m *janusSFU) NewRemoteSubscriber(ctx context.Context, listener sfu.Listener, publisher sfu.RemotePublisher) (sfu.RemoteSubscriber, error) { - pub, ok := publisher.(*janusRemotePublisher) - if !ok { - return nil, errors.New("unsupported remote publisher") - } - - session := m.session - if session == nil { - return nil, sfu.ErrNotConnected - } - - handle, err := session.Attach(ctx, pluginVideoRoom) - if err != nil { - return nil, err - } - - m.logger.Printf("Attached subscriber to room %d of publisher %s in plugin %s in session %d as %d", pub.roomId, pub.id, pluginVideoRoom, session.Id, handle.Id) - - client := &janusRemoteSubscriber{ - janusSubscriber: janusSubscriber{ - janusClient: janusClient{ - logger: m.logger, - mcu: m, - listener: listener, - - id: m.clientId.Add(1), - roomId: pub.roomId, - sid: strconv.FormatUint(handle.Id, 10), - streamType: publisher.StreamType(), - maxBitrate: pub.MaxBitrate(), - - closeChan: make(chan struct{}, 1), - deferred: make(chan func(), 64), - }, - publisher: pub.id, - }, - } - client.remote.Store(pub) - pub.addRef() - client.handle.Store(handle) - client.handleId.Store(handle.Id) - client.janusClient.handleEvent = client.handleEvent - client.janusClient.handleHangup = client.handleHangup - client.janusClient.handleDetached = client.handleDetached - client.janusClient.handleConnected = client.handleConnected - client.janusClient.handleSlowLink = client.handleSlowLink - client.janusClient.handleMedia = client.handleMedia - m.registerClient(client) - go client.run(handle, client.closeChan) - m.stats.IncSubscriber(publisher.StreamType()) - sfuinternal.StatsSubscribersTotal.WithLabelValues(string(publisher.StreamType())).Inc() - return client, nil -} - -func (m *janusSFU) UpdateBandwidth(handle uint64, media string, sent api.Bandwidth, received api.Bandwidth) { - m.muClients.RLock() - defer m.muClients.RUnlock() - - client, found := m.clients[handle] - if !found { - return - } - - client.UpdateBandwidth(media, sent, received) -} diff --git a/sfu/janus/janus_test.go b/sfu/janus/janus_test.go deleted file mode 100644 index 6852e66..0000000 --- a/sfu/janus/janus_test.go +++ /dev/null @@ -1,1733 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2024 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "context" - "encoding/json" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - metricstest "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" - janustest "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/test" -) - -const ( - testTimeout = 10 * time.Second -) - -func TestMcuJanusStats(t *testing.T) { - t.Parallel() - metricstest.CollectAndLint(t, janusMcuStats...) -} - -func newMcuJanusForTesting(t *testing.T) (*janusSFU, *janustest.JanusGateway) { - gateway := janustest.NewJanusGateway(t) - - config := goconf.NewConfigFile() - if strings.Contains(t.Name(), "Filter") { - config.AddOption("mcu", "blockedcandidates", "192.0.0.0/24, 192.168.0.0/16") - } - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - mcu, err := NewJanusSFU(ctx, "", config) - require.NoError(t, err) - t.Cleanup(func() { - mcu.Stop() - }) - - mcuJanus := mcu.(*janusSFU) - mcuJanus.createJanusGateway = func(ctx context.Context, wsURL string, listener janus.GatewayListener) (janus.GatewayInterface, error) { - return gateway, nil - } - require.NoError(t, mcu.Start(ctx)) - return mcuJanus, gateway -} - -type TestMcuListener struct { - id api.PublicSessionId - closed atomic.Bool - updatedOffer chan api.StringMap -} - -func NewTestMcuListener(id api.PublicSessionId) *TestMcuListener { - return &TestMcuListener{ - id: id, - updatedOffer: make(chan api.StringMap), - } -} - -func (t *TestMcuListener) PublicId() api.PublicSessionId { - return t.id -} - -func (t *TestMcuListener) OnUpdateOffer(client sfu.Client, offer api.StringMap) { - t.updatedOffer <- offer -} - -func (t *TestMcuListener) OnIceCandidate(client sfu.Client, candidate any) { - -} - -func (t *TestMcuListener) OnIceCompleted(client sfu.Client) { - -} - -func (t *TestMcuListener) SubscriberSidUpdated(subscriber sfu.Subscriber) { - -} - -func (t *TestMcuListener) PublisherClosed(publisher sfu.Publisher) { - -} - -func (t *TestMcuListener) SubscriberClosed(subscriber sfu.Subscriber) { - t.closed.Store(true) -} - -type TestMcuController struct { - id api.PublicSessionId -} - -func (c *TestMcuController) PublisherId() api.PublicSessionId { - return c.id -} - -func (c *TestMcuController) StartPublishing(ctx context.Context, publisher sfu.RemotePublisherProperties) error { - // TODO: Check parameters? - return nil -} - -func (c *TestMcuController) StopPublishing(ctx context.Context, publisher sfu.RemotePublisherProperties) error { - // TODO: Check parameters? - return nil -} - -func (c *TestMcuController) GetStreams(ctx context.Context) ([]sfu.PublisherStream, error) { - streams := []sfu.PublisherStream{ - { - Mid: "0", - Mindex: 0, - Type: "audio", - Codec: "opus", - }, - } - return streams, nil -} - -type TestMcuInitiator struct { - country geoip.Country -} - -func (i *TestMcuInitiator) Country() geoip.Country { - return i.country -} - -func Test_JanusPublisherFilterOffer(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - if assert.NotNil(jsep) { - // The SDP received by Janus will be filtered from blocked candidates. - if sdpValue, found := jsep["sdp"]; assert.True(found) { - sdpText, ok := sdpValue.(string) - if assert.True(ok) { - assert.Equal(mock.MockSdpOfferAudioOnlyNoFilter, strings.ReplaceAll(sdpText, "\r\n", "\n")) - } - } - } - - return &janus.EventMsg{ - Jsep: api.StringMap{ - "sdp": mock.MockSdpAnswerAudioOnly, - }, - }, nil - }, - "trickle": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - return &janus.AckMsg{}, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - defer pub.Close(context.Background()) - - // Send offer containing candidates that will be blocked / filtered. - data := &api.MessageClientMessageData{ - Type: "offer", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioOnly, - }, - } - require.NoError(data.CheckValid()) - - var wg sync.WaitGroup - wg.Add(1) - pub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer wg.Done() - - if assert.NoError(err) { - if sdpValue, found := m["sdp"]; assert.True(found) { - sdpText, ok := sdpValue.(string) - if assert.True(ok) { - assert.Equal(mock.MockSdpAnswerAudioOnly, strings.ReplaceAll(sdpText, "\r\n", "\n")) - } - } - } - }) - wg.Wait() - - data = &api.MessageClientMessageData{ - Type: "candidate", - Payload: api.StringMap{ - "candidate": api.StringMap{ - "candidate": "candidate:1 1 UDP 1685987071 192.168.0.1 49203 typ srflx raddr 198.51.100.7 rport 51556", - }, - }, - } - require.NoError(data.CheckValid()) - wg.Add(1) - pub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer wg.Done() - - assert.ErrorContains(err, "filtered") - assert.Empty(m) - }) - wg.Wait() - - data = &api.MessageClientMessageData{ - Type: "candidate", - Payload: api.StringMap{ - "candidate": api.StringMap{ - "candidate": "candidate:0 1 UDP 2122194687 198.51.100.7 51556 typ host", - }, - }, - } - require.NoError(data.CheckValid()) - wg.Add(1) - pub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer wg.Done() - - assert.NoError(err) - assert.Empty(m) - }) - wg.Wait() -} - -func Test_JanusSubscriberFilterAnswer(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "start": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - if assert.NotNil(jsep) { - // The SDP received by Janus will be filtered from blocked candidates. - if sdpValue, found := jsep["sdp"]; assert.True(found) { - sdpText, ok := sdpValue.(string) - if assert.True(ok) { - assert.Equal(mock.MockSdpAnswerAudioOnlyNoFilter, strings.ReplaceAll(sdpText, "\r\n", "\n")) - } - } - } - - return &janus.EventMsg{ - Plugindata: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "room": room.Id(), - "started": true, - "videoroom": "event", - }, - }, - }, nil - }, - "trickle": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - return &janus.AckMsg{}, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - defer pub.Close(context.Background()) - - listener2 := &TestMcuListener{ - id: pubId, - } - - initiator2 := &TestMcuInitiator{ - country: "DE", - } - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - defer sub.Close(context.Background()) - - // Send answer containing candidates that will be blocked / filtered. - data := &api.MessageClientMessageData{ - Type: "answer", - Payload: api.StringMap{ - "sdp": mock.MockSdpAnswerAudioOnly, - }, - } - require.NoError(data.CheckValid()) - - var wg sync.WaitGroup - wg.Add(1) - sub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer wg.Done() - - if assert.NoError(err) { - assert.Empty(m) - } - }) - wg.Wait() - - data = &api.MessageClientMessageData{ - Type: "candidate", - Payload: api.StringMap{ - "candidate": api.StringMap{ - "candidate": "candidate:1 1 UDP 1685987071 192.168.0.1 49203 typ srflx raddr 198.51.100.7 rport 51556", - }, - }, - } - require.NoError(data.CheckValid()) - wg.Add(1) - sub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer wg.Done() - - assert.ErrorContains(err, "filtered") - assert.Empty(m) - }) - wg.Wait() - - data = &api.MessageClientMessageData{ - Type: "candidate", - Payload: api.StringMap{ - "candidate": api.StringMap{ - "candidate": "candidate:0 1 UDP 2122194687 198.51.100.7 51556 typ host", - }, - }, - } - require.NoError(data.CheckValid()) - wg.Add(1) - sub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer wg.Done() - - assert.NoError(err) - assert.Empty(m) - }) - wg.Wait() -} - -func Test_JanusPublisherGetStreamsAudioOnly(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - if assert.NotNil(jsep) { - if sdpValue, found := jsep["sdp"]; assert.True(found) { - sdpText, ok := sdpValue.(string) - if assert.True(ok) { - assert.Equal(mock.MockSdpOfferAudioOnly, strings.ReplaceAll(sdpText, "\r\n", "\n")) - } - } - } - - return &janus.EventMsg{ - Jsep: api.StringMap{ - "sdp": mock.MockSdpAnswerAudioOnly, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - defer pub.Close(context.Background()) - - data := &api.MessageClientMessageData{ - Type: "offer", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioOnly, - }, - } - require.NoError(data.CheckValid()) - - done := make(chan struct{}) - pub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer close(done) - - if assert.NoError(err) { - if sdpValue, found := m["sdp"]; assert.True(found) { - sdpText, ok := sdpValue.(string) - if assert.True(ok) { - assert.Equal(mock.MockSdpAnswerAudioOnly, strings.ReplaceAll(sdpText, "\r\n", "\n")) - } - } - } - }) - <-done - - if sb, ok := pub.(*janusPublisher); assert.True(ok, "expected publisher with streams support, got %T", pub) { - if streams, err := sb.GetStreams(ctx); assert.NoError(err) { - if assert.Len(streams, 1) { - stream := streams[0] - assert.Equal("audio", stream.Type) - assert.Equal("audio", stream.Mid) - assert.Equal(0, stream.Mindex) - assert.False(stream.Disabled) - assert.Equal("opus", stream.Codec) - assert.False(stream.Stereo) - assert.False(stream.Fec) - assert.False(stream.Dtx) - } - } - } -} - -func Test_JanusPublisherGetStreamsAudioVideo(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - if assert.NotNil(jsep) { - _, found := jsep["sdp"] - assert.True(found) - } - - return &janus.EventMsg{ - Jsep: api.StringMap{ - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - defer pub.Close(context.Background()) - - data := &api.MessageClientMessageData{ - Type: "offer", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - } - require.NoError(data.CheckValid()) - - // Defer sending of offer / answer so "GetStreams" will wait. - go func() { - done := make(chan struct{}) - pub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer close(done) - - if assert.NoError(err) { - if sdpValue, found := m["sdp"]; assert.True(found) { - sdpText, ok := sdpValue.(string) - if assert.True(ok) { - assert.Equal(mock.MockSdpAnswerAudioAndVideo, strings.ReplaceAll(sdpText, "\r\n", "\n")) - } - } - } - }) - <-done - }() - - if sb, ok := pub.(*janusPublisher); assert.True(ok, "expected publisher with streams support, got %T", pub) { - if streams, err := sb.GetStreams(ctx); assert.NoError(err) { - if assert.Len(streams, 2) { - stream := streams[0] - assert.Equal("audio", stream.Type) - assert.Equal("audio", stream.Mid) - assert.Equal(0, stream.Mindex) - assert.False(stream.Disabled) - assert.Equal("opus", stream.Codec) - assert.False(stream.Stereo) - assert.False(stream.Fec) - assert.False(stream.Dtx) - - stream = streams[1] - assert.Equal("video", stream.Type) - assert.Equal("video", stream.Mid) - assert.Equal(1, stream.Mindex) - assert.False(stream.Disabled) - assert.Equal("H264", stream.Codec) - assert.Equal("4d0028", stream.ProfileH264) - } - } - } -} - -type mockBandwidthStats struct { - incoming uint64 - outgoing uint64 -} - -func (s *mockBandwidthStats) SetBandwidth(incoming uint64, outgoing uint64) { - s.incoming = incoming - s.outgoing = outgoing -} - -func Test_JanusPublisherSubscriber(t *testing.T) { - t.Parallel() - - stats := &mockBandwidthStats{} - require := require.New(t) - assert := assert.New(t) - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{}) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - // Bandwidth for unknown handles is ignored. - mcu.UpdateBandwidth(1234, "video", api.BandwidthFromBytes(100), api.BandwidthFromBytes(200)) - mcu.updateBandwidthStats(stats) - assert.EqualValues(0, stats.incoming) - assert.EqualValues(0, stats.outgoing) - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - defer pub.Close(context.Background()) - - janusPub, ok := pub.(*janusPublisher) - require.True(ok) - - assert.Nil(mcu.Bandwidth()) - assert.Nil(janusPub.Bandwidth()) - mcu.UpdateBandwidth(janusPub.Handle(), "video", api.BandwidthFromBytes(1000), api.BandwidthFromBytes(2000)) - if bw := janusPub.Bandwidth(); assert.NotNil(bw) { - assert.Equal(api.BandwidthFromBytes(1000), bw.Sent) - assert.Equal(api.BandwidthFromBytes(2000), bw.Received) - } - if bw := mcu.Bandwidth(); assert.NotNil(bw) { - assert.Equal(api.BandwidthFromBytes(1000), bw.Sent) - assert.Equal(api.BandwidthFromBytes(2000), bw.Received) - } - mcu.updateBandwidthStats(stats) - assert.EqualValues(2000, stats.incoming) - assert.EqualValues(1000, stats.outgoing) - - listener2 := &TestMcuListener{ - id: pubId, - } - - initiator2 := &TestMcuInitiator{ - country: "DE", - } - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - defer sub.Close(context.Background()) - - janusSub, ok := sub.(*janusSubscriber) - require.True(ok) - - assert.Nil(janusSub.Bandwidth()) - mcu.UpdateBandwidth(janusSub.Handle(), "video", api.BandwidthFromBytes(3000), api.BandwidthFromBytes(4000)) - if bw := janusSub.Bandwidth(); assert.NotNil(bw) { - assert.Equal(api.BandwidthFromBytes(3000), bw.Sent) - assert.Equal(api.BandwidthFromBytes(4000), bw.Received) - } - if bw := mcu.Bandwidth(); assert.NotNil(bw) { - assert.Equal(api.BandwidthFromBytes(4000), bw.Sent) - assert.Equal(api.BandwidthFromBytes(6000), bw.Received) - } - assert.EqualValues(2000, stats.incoming) - assert.EqualValues(1000, stats.outgoing) - mcu.updateBandwidthStats(stats) - assert.EqualValues(6000, stats.incoming) - assert.EqualValues(4000, stats.outgoing) -} - -func Test_JanusSubscriberPublisher(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{}) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - ready := make(chan struct{}) - done := make(chan struct{}) - - go func() { - defer close(done) - time.Sleep(100 * time.Millisecond) - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - if !assert.NoError(err) { - return - } - - defer func() { - <-ready - pub.Close(context.Background()) - }() - }() - - listener2 := &TestMcuListener{ - id: pubId, - } - - initiator2 := &TestMcuInitiator{ - country: "DE", - } - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - defer sub.Close(context.Background()) - close(ready) - <-done -} - -func Test_JanusSubscriberRequestOffer(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - var originalOffer atomic.Value - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - if assert.NotNil(jsep) { - if sdp, found := jsep["sdp"]; assert.True(found) { - originalOffer.Store(strings.ReplaceAll(sdp.(string), "\r\n", "\n")) - } - } - - return &janus.EventMsg{ - Jsep: api.StringMap{ - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - defer pub.Close(context.Background()) - - listener2 := &TestMcuListener{ - id: pubId, - } - - initiator2 := &TestMcuInitiator{ - country: "DE", - } - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - defer sub.Close(context.Background()) - - go func() { - data := &api.MessageClientMessageData{ - Type: "offer", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - } - if !assert.NoError(data.CheckValid()) { - return - } - - done := make(chan struct{}) - pub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer close(done) - - if assert.NoError(err) { - if sdpValue, found := m["sdp"]; assert.True(found) { - sdpText, ok := sdpValue.(string) - if assert.True(ok) { - assert.Equal(mock.MockSdpAnswerAudioAndVideo, strings.ReplaceAll(sdpText, "\r\n", "\n")) - } - } - } - }) - <-done - }() - - data := &api.MessageClientMessageData{ - Type: "requestoffer", - } - require.NoError(data.CheckValid()) - - done := make(chan struct{}) - sub.SendMessage(ctx, &api.MessageClientMessage{}, data, func(err error, m api.StringMap) { - defer close(done) - - if assert.NoError(err) { - if sdpValue, found := m["sdp"]; assert.True(found) { - sdpText, ok := sdpValue.(string) - if assert.True(ok) { - if sdp := originalOffer.Load(); assert.NotNil(sdp) { - assert.Equal(sdp.(string), strings.ReplaceAll(sdpText, "\r\n", "\n")) - } - } - } - } - }) - <-done -} - -func Test_JanusRemotePublisher(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - - var added atomic.Int32 - var removed atomic.Int32 - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "add_remote_publisher": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - assert.Nil(jsep) - if streams := body["streams"].([]any); assert.Len(streams, 1) { - if stream, ok := api.ConvertStringMap(streams[0]); assert.True(ok, "not a string map: %+v", streams[0]) { - assert.Equal("0", stream["mid"]) - assert.EqualValues(0, stream["mindex"]) - assert.Equal("audio", stream["type"]) - assert.Equal("opus", stream["codec"]) - } - } - added.Add(1) - return &janus.SuccessMsg{ - PluginData: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "id": 12345, - "port": 10000, - "rtcp_port": 10001, - }, - }, - }, nil - }, - "remove_remote_publisher": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - assert.Nil(jsep) - removed.Add(1) - return &janus.SuccessMsg{ - PluginData: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{}, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - listener1 := &TestMcuListener{ - id: "publisher-id", - } - - controller := &TestMcuController{ - id: listener1.id, - } - - pub, err := mcu.NewRemotePublisher(ctx, listener1, controller, sfu.StreamTypeVideo) - require.NoError(err) - defer pub.Close(context.Background()) - - assert.EqualValues(1, added.Load()) - assert.EqualValues(0, removed.Load()) - - listener2 := &TestMcuListener{ - id: "subscriber-id", - } - - sub, err := mcu.NewRemoteSubscriber(ctx, listener2, pub) - require.NoError(err) - defer sub.Close(context.Background()) - - pub.Close(context.Background()) - - assert.EqualValues(1, added.Load()) - // The publisher is ref-counted, and still referenced by the subscriber. - assert.EqualValues(0, removed.Load()) - - sub.Close(context.Background()) - - assert.EqualValues(1, added.Load()) - assert.EqualValues(1, removed.Load()) -} - -type mockJanusStats struct { - called atomic.Bool - - mu sync.Mutex - // +checklocks:mu - value map[sfu.StreamType]int -} - -func (s *mockJanusStats) Value(streamType sfu.StreamType) int { - s.mu.Lock() - defer s.mu.Unlock() - - return s.value[streamType] -} - -func (s *mockJanusStats) IncSubscriber(streamType sfu.StreamType) { - s.called.Store(true) - - s.mu.Lock() - defer s.mu.Unlock() - - if s.value == nil { - s.value = make(map[sfu.StreamType]int) - } - s.value[streamType]++ -} - -func (s *mockJanusStats) DecSubscriber(streamType sfu.StreamType) { - s.called.Store(true) - - s.mu.Lock() - defer s.mu.Unlock() - - if s.value == nil { - s.value = make(map[sfu.StreamType]int) - } - s.value[streamType]-- -} - -func Test_SubscriberNoSuchRoom(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - stats := &mockJanusStats{} - - t.Cleanup(func() { - if !t.Failed() { - assert.True(stats.called.Load(), "stats were not called") - assert.Equal(0, stats.Value("video")) - } - }) - - mcu, gateway := newMcuJanusForTesting(t) - mcu.SetStats(stats) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - return &janus.EventMsg{ - Jsep: api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - - defer pub.Close(context.Background()) - - msgData := &api.MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - } - require.NoError(msgData.CheckValid()) - payload, err := json.Marshal(msgData) - require.NoError(err) - msg := &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - ch := make(chan struct{}) - pub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpAnswerAudioAndVideo, sm["sdp"]) - }) - <-ch - - listener2 := &TestMcuListener{ - id: pubId, - } - initiator2 := &TestMcuInitiator{ - country: "DE", - } - - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - msgData = &api.MessageClientMessageData{ - Type: "requestoffer", - RoomType: "video", - } - require.NoError(msgData.CheckValid()) - payload, err = json.Marshal(msgData) - require.NoError(err) - msg = &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.ErrorContains(err, "not created yet for") - assert.Empty(sm) - }) - <-ch - - assert.True(listener2.closed.Load()) - - listener3 := &TestMcuListener{ - id: pubId, - } - - sub, err = mcu.NewSubscriber(ctx, listener3, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpOfferAudioAndVideo, sm["sdp"]) - }) - <-ch - - assert.False(listener3.closed.Load()) -} - -func test_JanusSubscriberAlreadyJoined(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - stats := &mockJanusStats{} - - t.Cleanup(func() { - if !t.Failed() { - assert.True(stats.called.Load(), "stats were not called") - assert.Equal(0, stats.Value("video")) - } - }) - - mcu, gateway := newMcuJanusForTesting(t) - mcu.SetStats(stats) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - return &janus.EventMsg{ - Jsep: api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - - defer pub.Close(context.Background()) - - msgData := &api.MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - } - require.NoError(msgData.CheckValid()) - payload, err := json.Marshal(msgData) - require.NoError(err) - msg := &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - ch := make(chan struct{}) - pub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpAnswerAudioAndVideo, sm["sdp"]) - }) - <-ch - - listener2 := &TestMcuListener{ - id: pubId, - } - initiator2 := &TestMcuInitiator{ - country: "DE", - } - - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - msgData = &api.MessageClientMessageData{ - Type: "requestoffer", - RoomType: "video", - } - require.NoError(msgData.CheckValid()) - payload, err = json.Marshal(msgData) - require.NoError(err) - msg = &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - if strings.Contains(t.Name(), "AttachError") { - assert.ErrorContains(err, "already connected as subscriber for") - assert.Empty(sm) - } else { - assert.NoError(err) - assert.Equal(mock.MockSdpOfferAudioAndVideo, sm["sdp"]) - } - }) - <-ch - - if strings.Contains(t.Name(), "AttachError") { - assert.True(listener2.closed.Load()) - - listener3 := &TestMcuListener{ - id: pubId, - } - - sub, err := mcu.NewSubscriber(ctx, listener3, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpOfferAudioAndVideo, sm["sdp"]) - }) - <-ch - } -} - -func Test_SubscriberAlreadyJoined(t *testing.T) { - t.Parallel() - test_JanusSubscriberAlreadyJoined(t) -} - -func Test_SubscriberAlreadyJoinedAttachError(t *testing.T) { - t.Parallel() - test_JanusSubscriberAlreadyJoined(t) -} - -func Test_SubscriberTimeout(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - stats := &mockJanusStats{} - - t.Cleanup(func() { - if !t.Failed() { - assert.True(stats.called.Load(), "stats were not called") - assert.Equal(0, stats.Value("video")) - } - }) - - mcu, gateway := newMcuJanusForTesting(t) - mcu.SetStats(stats) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - return &janus.EventMsg{ - Jsep: api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - - defer pub.Close(context.Background()) - - msgData := &api.MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - } - require.NoError(msgData.CheckValid()) - payload, err := json.Marshal(msgData) - require.NoError(err) - msg := &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - ch := make(chan struct{}) - pub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpAnswerAudioAndVideo, sm["sdp"]) - }) - <-ch - - oldTimeout := mcu.Settings().Timeout() - mcu.Settings().SetTimeout(100 * time.Millisecond) - - listener2 := &TestMcuListener{ - id: pubId, - } - initiator2 := &TestMcuInitiator{ - country: "DE", - } - - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - msgData = &api.MessageClientMessageData{ - Type: "requestoffer", - RoomType: "video", - } - require.NoError(msgData.CheckValid()) - payload, err = json.Marshal(msgData) - require.NoError(err) - msg = &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.ErrorIs(err, context.DeadlineExceeded) - assert.Empty(sm) - }) - <-ch - - assert.True(listener2.closed.Load()) - - mcu.Settings().SetTimeout(oldTimeout) - - listener3 := &TestMcuListener{ - id: pubId, - } - - sub, err = mcu.NewSubscriber(ctx, listener3, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpOfferAudioAndVideo, sm["sdp"]) - }) - <-ch -} - -func Test_SubscriberCloseEmptyStreams(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - stats := &mockJanusStats{} - - t.Cleanup(func() { - if !t.Failed() { - assert.True(stats.called.Load(), "stats were not called") - assert.Equal(0, stats.Value("video")) - } - }) - - mcu, gateway := newMcuJanusForTesting(t) - mcu.SetStats(stats) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - return &janus.EventMsg{ - Jsep: api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - - defer pub.Close(context.Background()) - - msgData := &api.MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - } - require.NoError(msgData.CheckValid()) - payload, err := json.Marshal(msgData) - require.NoError(err) - msg := &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - ch := make(chan struct{}) - pub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpAnswerAudioAndVideo, sm["sdp"]) - }) - <-ch - - listener2 := &TestMcuListener{ - id: pubId, - } - initiator2 := &TestMcuInitiator{ - country: "DE", - } - - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - msgData = &api.MessageClientMessageData{ - Type: "requestoffer", - RoomType: "video", - } - require.NoError(msgData.CheckValid()) - payload, err = json.Marshal(msgData) - require.NoError(err) - msg = &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpOfferAudioAndVideo, sm["sdp"]) - }) - <-ch - - subscriber, ok := sub.(*janusSubscriber) - require.True(ok) - handle := subscriber.JanusHandle() - require.NotNil(handle) - - for ctx.Err() == nil { - if handle = subscriber.JanusHandle(); handle == nil && listener2.closed.Load() { - break - } - - time.Sleep(time.Millisecond) - } - - assert.Nil(handle, "subscriber should have been closed") - assert.True(listener2.closed.Load()) -} - -func Test_SubscriberRoomDestroyed(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - stats := &mockJanusStats{} - - t.Cleanup(func() { - if !t.Failed() { - assert.True(stats.called.Load(), "stats were not called") - assert.Equal(0, stats.Value("video")) - } - }) - - mcu, gateway := newMcuJanusForTesting(t) - mcu.SetStats(stats) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - return &janus.EventMsg{ - Jsep: api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - - defer pub.Close(context.Background()) - - msgData := &api.MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - } - require.NoError(msgData.CheckValid()) - payload, err := json.Marshal(msgData) - require.NoError(err) - msg := &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - ch := make(chan struct{}) - pub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpAnswerAudioAndVideo, sm["sdp"]) - }) - <-ch - - listener2 := &TestMcuListener{ - id: pubId, - } - initiator2 := &TestMcuInitiator{ - country: "DE", - } - - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - msgData = &api.MessageClientMessageData{ - Type: "requestoffer", - RoomType: "video", - } - require.NoError(msgData.CheckValid()) - payload, err = json.Marshal(msgData) - require.NoError(err) - msg = &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpOfferAudioAndVideo, sm["sdp"]) - }) - <-ch - - subscriber, ok := sub.(*janusSubscriber) - require.True(ok) - handle := subscriber.JanusHandle() - require.NotNil(handle) - - for ctx.Err() == nil { - if handle = subscriber.JanusHandle(); handle == nil && listener2.closed.Load() { - break - } - - time.Sleep(time.Millisecond) - } - - assert.Nil(handle, "subscriber should have been closed") - assert.True(listener2.closed.Load()) -} - -func Test_SubscriberUpdateOffer(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - stats := &mockJanusStats{} - - t.Cleanup(func() { - if !t.Failed() { - assert.True(stats.called.Load(), "stats were not called") - assert.Equal(0, stats.Value("video")) - } - }) - - mcu, gateway := newMcuJanusForTesting(t) - mcu.SetStats(stats) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "configure": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - assert.EqualValues(1, room.Id()) - return &janus.EventMsg{ - Jsep: api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioAndVideo, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - - defer pub.Close(context.Background()) - - msgData := &api.MessageClientMessageData{ - Type: "offer", - RoomType: "video", - Payload: api.StringMap{ - "sdp": mock.MockSdpOfferAudioAndVideo, - }, - } - require.NoError(msgData.CheckValid()) - payload, err := json.Marshal(msgData) - require.NoError(err) - msg := &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - ch := make(chan struct{}) - pub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpAnswerAudioAndVideo, sm["sdp"]) - }) - <-ch - - listener2 := NewTestMcuListener(pubId) - initiator2 := &TestMcuInitiator{ - country: "DE", - } - - sub, err := mcu.NewSubscriber(ctx, listener2, pubId, sfu.StreamTypeVideo, initiator2) - require.NoError(err) - - defer sub.Close(context.Background()) - - msgData = &api.MessageClientMessageData{ - Type: "requestoffer", - RoomType: "video", - } - require.NoError(msgData.CheckValid()) - payload, err = json.Marshal(msgData) - require.NoError(err) - msg = &api.MessageClientMessage{ - Recipient: api.MessageClientMessageRecipient{ - Type: "session", - SessionId: pubId, - }, - Data: payload, - } - - sub.SendMessage(ctx, msg, msgData, func(err error, sm api.StringMap) { - defer func() { - ch <- struct{}{} - }() - - assert.NoError(err) - assert.Equal(mock.MockSdpOfferAudioAndVideo, sm["sdp"]) - }) - <-ch - - // Test MCU will trigger an updated offer. - select { - case offer := <-listener2.updatedOffer: - assert.Equal(mock.MockSdpOfferAudioOnly, offer["sdp"]) - case <-ctx.Done(): - assert.NoError(ctx.Err()) - } -} diff --git a/sfu/janus/publisher_stats_counter.go b/sfu/janus/publisher_stats_counter.go deleted file mode 100644 index 57ebeed..0000000 --- a/sfu/janus/publisher_stats_counter.go +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "sync" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" -) - -type publisherStatsCounterStats interface { - IncPublisherStream(streamType sfu.StreamType) - DecPublisherStream(streamType sfu.StreamType) - IncSubscriberStream(streamType sfu.StreamType) - DecSubscriberStream(streamType sfu.StreamType) - AddSubscriberStreams(streamType sfu.StreamType, count int) - SubSubscriberStreams(streamType sfu.StreamType, count int) -} - -type prometheusPublisherStats struct{} - -func (s *prometheusPublisherStats) IncPublisherStream(streamType sfu.StreamType) { - statsMcuPublisherStreamTypesCurrent.WithLabelValues(string(streamType)).Inc() -} - -func (s *prometheusPublisherStats) DecPublisherStream(streamType sfu.StreamType) { - statsMcuPublisherStreamTypesCurrent.WithLabelValues(string(streamType)).Dec() -} - -func (s *prometheusPublisherStats) IncSubscriberStream(streamType sfu.StreamType) { - statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Inc() -} - -func (s *prometheusPublisherStats) DecSubscriberStream(streamType sfu.StreamType) { - statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Dec() -} - -func (s *prometheusPublisherStats) AddSubscriberStreams(streamType sfu.StreamType, count int) { - statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Add(float64(count)) -} - -func (s *prometheusPublisherStats) SubSubscriberStreams(streamType sfu.StreamType, count int) { - statsMcuSubscriberStreamTypesCurrent.WithLabelValues(string(streamType)).Sub(float64(count)) -} - -var ( - defaultPublisherStats = &prometheusPublisherStats{} // +checklocksignore: Global readonly variable. -) - -type publisherStatsCounter struct { - mu sync.Mutex - - // +checklocks:mu - streamTypes map[sfu.StreamType]bool - // +checklocks:mu - subscribers map[string]bool - // +checklocks:mu - stats publisherStatsCounterStats -} - -func (c *publisherStatsCounter) Reset() { - c.mu.Lock() - defer c.mu.Unlock() - - stats := c.stats - if stats == nil { - stats = defaultPublisherStats - } - - count := len(c.subscribers) - for streamType := range c.streamTypes { - stats.DecPublisherStream(streamType) - stats.SubSubscriberStreams(streamType, count) - } - c.streamTypes = nil - c.subscribers = nil -} - -func (c *publisherStatsCounter) EnableStream(streamType sfu.StreamType, enable bool) { - c.mu.Lock() - defer c.mu.Unlock() - - if enable == c.streamTypes[streamType] { - return - } - - stats := c.stats - if stats == nil { - stats = defaultPublisherStats - } - - if enable { - if c.streamTypes == nil { - c.streamTypes = make(map[sfu.StreamType]bool) - } - c.streamTypes[streamType] = true - stats.IncPublisherStream(streamType) - stats.AddSubscriberStreams(streamType, len(c.subscribers)) - } else { - delete(c.streamTypes, streamType) - stats.DecPublisherStream(streamType) - stats.SubSubscriberStreams(streamType, len(c.subscribers)) - } -} - -func (c *publisherStatsCounter) AddSubscriber(id string) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.subscribers[id] { - return - } - - stats := c.stats - if stats == nil { - stats = defaultPublisherStats - } - - if c.subscribers == nil { - c.subscribers = make(map[string]bool) - } - c.subscribers[id] = true - for streamType := range c.streamTypes { - stats.IncSubscriberStream(streamType) - } -} - -func (c *publisherStatsCounter) RemoveSubscriber(id string) { - c.mu.Lock() - defer c.mu.Unlock() - - if !c.subscribers[id] { - return - } - - stats := c.stats - if stats == nil { - stats = defaultPublisherStats - } - - delete(c.subscribers, id) - for streamType := range c.streamTypes { - stats.DecSubscriberStream(streamType) - } -} diff --git a/sfu/janus/publisher_stats_counter_test.go b/sfu/janus/publisher_stats_counter_test.go deleted file mode 100644 index 3356440..0000000 --- a/sfu/janus/publisher_stats_counter_test.go +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" -) - -type mockPublisherStats struct { - publishers map[sfu.StreamType]int - subscribers map[sfu.StreamType]int -} - -func (s *mockPublisherStats) IncPublisherStream(streamType sfu.StreamType) { - if s.publishers == nil { - s.publishers = make(map[sfu.StreamType]int) - } - s.publishers[streamType]++ -} - -func (s *mockPublisherStats) DecPublisherStream(streamType sfu.StreamType) { - if s.publishers == nil { - s.publishers = make(map[sfu.StreamType]int) - } - s.publishers[streamType]-- -} - -func (s *mockPublisherStats) IncSubscriberStream(streamType sfu.StreamType) { - if s.subscribers == nil { - s.subscribers = make(map[sfu.StreamType]int) - } - s.subscribers[streamType]++ -} - -func (s *mockPublisherStats) DecSubscriberStream(streamType sfu.StreamType) { - if s.subscribers == nil { - s.subscribers = make(map[sfu.StreamType]int) - } - s.subscribers[streamType]-- -} - -func (s *mockPublisherStats) AddSubscriberStreams(streamType sfu.StreamType, count int) { - if s.subscribers == nil { - s.subscribers = make(map[sfu.StreamType]int) - } - s.subscribers[streamType] += count -} - -func (s *mockPublisherStats) SubSubscriberStreams(streamType sfu.StreamType, count int) { - if s.subscribers == nil { - s.subscribers = make(map[sfu.StreamType]int) - } - s.subscribers[streamType] -= count -} - -func (s *mockPublisherStats) Publishers(streamType sfu.StreamType) int { - return s.publishers[streamType] -} - -func (s *mockPublisherStats) Subscribers(streamType sfu.StreamType) int { - return s.subscribers[streamType] -} - -func TestPublisherStatsPrometheus(t *testing.T) { - t.Parallel() - - RegisterStats() - UnregisterStats() -} - -func TestPublisherStatsCounter(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - stats := &mockPublisherStats{} - c := publisherStatsCounter{ - stats: stats, - } - - c.Reset() - assert.Equal(0, stats.Publishers("audio")) - c.EnableStream("audio", false) - assert.Equal(0, stats.Publishers("audio")) - c.EnableStream("audio", true) - assert.Equal(1, stats.Publishers("audio")) - c.EnableStream("audio", true) - assert.Equal(1, stats.Publishers("audio")) - c.EnableStream("video", true) - assert.Equal(1, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - c.EnableStream("audio", false) - assert.Equal(0, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - c.EnableStream("audio", false) - assert.Equal(0, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - - c.AddSubscriber("1") - assert.Equal(0, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(0, stats.Subscribers("audio")) - assert.Equal(1, stats.Subscribers("video")) - c.EnableStream("audio", true) - assert.Equal(1, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(1, stats.Subscribers("audio")) - assert.Equal(1, stats.Subscribers("video")) - c.AddSubscriber("1") - assert.Equal(1, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(1, stats.Subscribers("audio")) - assert.Equal(1, stats.Subscribers("video")) - - c.AddSubscriber("2") - assert.Equal(1, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(2, stats.Subscribers("audio")) - assert.Equal(2, stats.Subscribers("video")) - - c.RemoveSubscriber("3") - assert.Equal(1, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(2, stats.Subscribers("audio")) - assert.Equal(2, stats.Subscribers("video")) - - c.RemoveSubscriber("1") - assert.Equal(1, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(1, stats.Subscribers("audio")) - assert.Equal(1, stats.Subscribers("video")) - - c.AddSubscriber("1") - assert.Equal(1, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(2, stats.Subscribers("audio")) - assert.Equal(2, stats.Subscribers("video")) - - c.EnableStream("audio", false) - assert.Equal(0, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(0, stats.Subscribers("audio")) - assert.Equal(2, stats.Subscribers("video")) - - c.EnableStream("audio", true) - assert.Equal(1, stats.Publishers("audio")) - assert.Equal(1, stats.Publishers("video")) - assert.Equal(2, stats.Subscribers("audio")) - assert.Equal(2, stats.Subscribers("video")) - - c.EnableStream("audio", false) - c.EnableStream("video", false) - assert.Equal(0, stats.Publishers("audio")) - assert.Equal(0, stats.Publishers("video")) - assert.Equal(0, stats.Subscribers("audio")) - assert.Equal(0, stats.Subscribers("video")) -} diff --git a/sfu/janus/publisher_test.go b/sfu/janus/publisher_test.go deleted file mode 100644 index 6dd5078..0000000 --- a/sfu/janus/publisher_test.go +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2024 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "context" - "sync/atomic" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" - janustest "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/test" -) - -func TestGetFmtpValueH264(t *testing.T) { - t.Parallel() - assert := assert.New(t) - testcases := []struct { - fmtp string - profile string - }{ - { - "", - "", - }, - { - "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", - "42001f", - }, - { - "level-asymmetry-allowed=1;packetization-mode=0", - "", - }, - { - "level-asymmetry-allowed=1; packetization-mode=0; profile-level-id = 42001f", - "42001f", - }, - } - - for _, tc := range testcases { - value, found := getFmtpValue(tc.fmtp, "profile-level-id") - if !found && tc.profile != "" { - assert.Fail("did not find profile", "profile \"%s\" in \"%s\"", tc.profile, tc.fmtp) - } else if found && tc.profile == "" { - assert.Fail("did not expect profile", "in \"%s\" but got \"%s\"", tc.fmtp, value) - } else if found && tc.profile != value { - assert.Fail("expected profile", "profile \"%s\" in \"%s\" but got \"%s\"", tc.profile, tc.fmtp, value) - } - } -} - -func TestGetFmtpValueVP9(t *testing.T) { - t.Parallel() - assert := assert.New(t) - testcases := []struct { - fmtp string - profile string - }{ - { - "", - "", - }, - { - "profile-id=0", - "0", - }, - { - "profile-id = 0", - "0", - }, - } - - for _, tc := range testcases { - value, found := getFmtpValue(tc.fmtp, "profile-id") - if !found && tc.profile != "" { - assert.Fail("did not find profile", "profile \"%s\" in \"%s\"", tc.profile, tc.fmtp) - } else if found && tc.profile == "" { - assert.Fail("did not expect profile", "in \"%s\" but got \"%s\"", tc.fmtp, value) - } else if found && tc.profile != value { - assert.Fail("expected profile", "profile \"%s\" in \"%s\" but got \"%s\"", tc.profile, tc.fmtp, value) - } - } -} - -func TestJanusPublisherRemote(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - var remotePublishId atomic.Value - - remoteId := api.PublicSessionId("the-remote-id") - hostname := "remote.server" - port := 12345 - rtcpPort := 23456 - - mcu, gateway := newMcuJanusForTesting(t) - gateway.RegisterHandlers(map[string]janustest.JanusHandler{ - "publish_remotely": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - if value, found := api.GetStringMapString[string](body, "host"); assert.True(found) { - assert.Equal(hostname, value) - } - if value, found := api.GetStringMapEntry[float64](body, "port"); assert.True(found) { - assert.InEpsilon(port, value, 0.0001) - } - if value, found := api.GetStringMapEntry[float64](body, "rtcp_port"); assert.True(found) { - assert.InEpsilon(rtcpPort, value, 0.0001) - } - if value, found := api.GetStringMapString[string](body, "remote_id"); assert.True(found) { - prev := remotePublishId.Swap(value) - assert.Nil(prev, "should not have previous value") - } - - return &janus.SuccessMsg{ - Data: janus.SuccessData{ - ID: 1, - }, - }, nil - }, - "unpublish_remotely": func(room *janustest.JanusRoom, body, jsep api.StringMap) (any, *janus.ErrorMsg) { - if value, found := api.GetStringMapString[string](body, "remote_id"); assert.True(found) { - if prev := remotePublishId.Load(); assert.NotNil(prev, "should have previous value") { - assert.Equal(prev, value) - } - } - return &janus.SuccessMsg{ - Data: janus.SuccessData{ - ID: 1, - }, - }, nil - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("publisher-id") - listener1 := &TestMcuListener{ - id: pubId, - } - - settings1 := sfu.NewPublisherSettings{} - initiator1 := &TestMcuInitiator{ - country: "DE", - } - - pub, err := mcu.NewPublisher(ctx, listener1, pubId, "sid", sfu.StreamTypeVideo, settings1, initiator1) - require.NoError(err) - defer pub.Close(context.Background()) - - require.Implements((*sfu.RemoteAwarePublisher)(nil), pub) - remotePub, _ := pub.(sfu.RemoteAwarePublisher) - - if assert.NoError(remotePub.PublishRemote(ctx, remoteId, hostname, port, rtcpPort)) { - assert.NoError(remotePub.UnpublishRemote(ctx, remoteId, hostname, port, rtcpPort)) - } -} diff --git a/sfu/janus/remote_publisher.go b/sfu/janus/remote_publisher.go deleted file mode 100644 index dd85657..0000000 --- a/sfu/janus/remote_publisher.go +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2024 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "context" - "sync/atomic" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" -) - -type janusRemotePublisher struct { - janusPublisher - - ref atomic.Int64 - - controller sfu.RemotePublisherController - - port int - rtcpPort int -} - -func (p *janusRemotePublisher) addRef() int64 { - return p.ref.Add(1) -} - -func (p *janusRemotePublisher) release() bool { - return p.ref.Add(-1) == 0 -} - -func (p *janusRemotePublisher) Port() int { - return p.port -} - -func (p *janusRemotePublisher) RtcpPort() int { - return p.rtcpPort -} - -func (p *janusRemotePublisher) handleEvent(event *janus.EventMsg) { - if videoroom := getPluginStringValue(event.Plugindata, pluginVideoRoom, "videoroom"); videoroom != "" { - ctx := context.TODO() - switch videoroom { - case "destroyed": - p.logger.Printf("Remote publisher %d: associated room has been destroyed, closing", p.handleId.Load()) - go p.Close(ctx) - case "slow_link": - // Ignore, processed through "handleSlowLink" in the general events. - default: - p.logger.Printf("Unsupported videoroom remote publisher event in %d: %+v", p.handleId.Load(), event) - } - } else { - p.logger.Printf("Unsupported remote publisher event in %d: %+v", p.handleId.Load(), event) - } -} - -func (p *janusRemotePublisher) handleHangup(event *janus.HangupMsg) { - p.logger.Printf("Remote publisher %d received hangup (%s), closing", p.handleId.Load(), event.Reason) - go p.Close(context.Background()) -} - -func (p *janusRemotePublisher) handleDetached(event *janus.DetachedMsg) { - p.logger.Printf("Remote publisher %d received detached, closing", p.handleId.Load()) - go p.Close(context.Background()) -} - -func (p *janusRemotePublisher) handleConnected(event *janus.WebRTCUpMsg) { - p.logger.Printf("Remote publisher %d received connected", p.handleId.Load()) - p.mcu.notifyPublisherConnected(p.id, p.streamType) -} - -func (p *janusRemotePublisher) handleSlowLink(event *janus.SlowLinkMsg) { - if event.Uplink { - p.logger.Printf("Remote publisher %s (%d) is reporting %d lost packets on the uplink (Janus -> client)", p.listener.PublicId(), p.handleId.Load(), event.Lost) - } else { - p.logger.Printf("Remote publisher %s (%d) is reporting %d lost packets on the downlink (client -> Janus)", p.listener.PublicId(), p.handleId.Load(), event.Lost) - } -} - -func (p *janusRemotePublisher) NotifyReconnected() { - ctx := context.TODO() - handle, session, roomId, _, err := p.mcu.getOrCreatePublisherHandle(ctx, p.id, p.streamType, p.settings) - if err != nil { - p.logger.Printf("Could not reconnect remote publisher %s: %s", p.id, err) - // TODO(jojo): Retry - return - } - - if prev := p.handle.Swap(handle); prev != nil { - if _, err := prev.Detach(context.Background()); err != nil { - p.logger.Printf("Error detaching old remote publisher handle %d: %s", prev.Id, err) - } - } - p.handleId.Store(handle.Id) - p.session = session - p.roomId = roomId - - p.logger.Printf("Remote publisher %s reconnected on handle %d", p.id, p.handleId.Load()) -} - -func (p *janusRemotePublisher) Close(ctx context.Context) { - if !p.release() { - return - } - - if err := p.controller.StopPublishing(ctx, p); err != nil { - p.logger.Printf("Error stopping remote publisher %s in room %d: %s", p.id, p.roomId, err) - } - - p.mu.Lock() - defer p.mu.Unlock() - - if handle := p.handle.Load(); handle != nil { - response, err := handle.Request(ctx, api.StringMap{ - "request": "remove_remote_publisher", - "room": p.roomId, - "id": streamTypeUserIds[p.streamType], - }) - if err != nil { - p.logger.Printf("Error removing remote publisher %s in room %d: %s", p.id, p.roomId, err) - } else { - p.logger.Printf("Removed remote publisher: %+v", response) - } - if p.roomId != 0 { - destroy_msg := api.StringMap{ - "request": "destroy", - "room": p.roomId, - } - if _, err := handle.Request(ctx, destroy_msg); err != nil { - p.logger.Printf("Error destroying room %d: %s", p.roomId, err) - } else { - p.logger.Printf("Room %d destroyed", p.roomId) - } - p.mcu.mu.Lock() - delete(p.mcu.remotePublishers, sfu.GetStreamId(p.id, p.streamType)) - p.mcu.mu.Unlock() - p.roomId = 0 - } - } - - p.closeClient(ctx) -} diff --git a/sfu/janus/stats_prometheus.go b/sfu/janus/stats_prometheus.go deleted file mode 100644 index 312f11d..0000000 --- a/sfu/janus/stats_prometheus.go +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/internal" -) - -var ( - statsMcuSubscriberStreamTypesCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "subscriber_streams", - Help: "The current number of subscribed media streams", - }, []string{"type"}) - statsMcuPublisherStreamTypesCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "publisher_streams", - Help: "The current number of published media streams", - }, []string{"type"}) - statsJanusBandwidthCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "bandwidth", - Help: "The current bandwidth in bytes per second", - }, []string{"direction"}) - statsJanusSelectedCandidateTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "selected_candidate_total", - Help: "Total number of selected candidates", - }, []string{"origin", "type", "transport", "family"}) - statsJanusPeerConnectionStateTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "peerconnection_state_total", - Help: "Total number of PeerConnections states", - }, []string{"state", "reason"}) - statsJanusICEStateTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "ice_state_total", - Help: "Total number of ICE connection states", - }, []string{"state"}) - statsJanusDTLSStateTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "dtls_state_total", - Help: "Total number of DTLS connection states", - }, []string{"state"}) - statsJanusSlowLinkTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "slow_link_total", - Help: "Total number of slow link events", - }, []string{"media", "direction"}) - statsJanusMediaRTT = prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "media_rtt", - Help: "The roundtrip time of WebRTC media in milliseconds", - Buckets: prometheus.ExponentialBucketsRange(1, 10000, 25), - }, []string{"media"}) - statsJanusMediaJitter = prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "media_jitter", - Help: "The jitter of WebRTC media in milliseconds", - Buckets: prometheus.ExponentialBucketsRange(1, 2000, 20), - }, []string{"media", "origin"}) - statsJanusMediaCodecsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "media_codecs_total", - Help: "The total number of codecs", - }, []string{"media", "codec"}) - statsJanusMediaNACKTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "media_nacks_total", - Help: "The total number of NACKs", - }, []string{"media", "direction"}) - statsJanusMediaRetransmissionsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "media_retransmissions_total", - Help: "The total number of received retransmissions", - }, []string{"media"}) - statsJanusMediaBytesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "media_bytes_total", - Help: "The total number of media bytes sent / received", - }, []string{"media", "direction"}) - statsJanusMediaLostTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "media_lost_total", - Help: "The total number of lost media packets", - }, []string{"media", "origin"}) - - janusMcuStats = []prometheus.Collector{ - statsMcuSubscriberStreamTypesCurrent, - statsMcuPublisherStreamTypesCurrent, - statsJanusBandwidthCurrent, - statsJanusSelectedCandidateTotal, - statsJanusPeerConnectionStateTotal, - statsJanusICEStateTotal, - statsJanusDTLSStateTotal, - statsJanusSlowLinkTotal, - statsJanusMediaRTT, - statsJanusMediaJitter, - statsJanusMediaCodecsTotal, - statsJanusMediaNACKTotal, - statsJanusMediaRetransmissionsTotal, - statsJanusMediaBytesTotal, - statsJanusMediaLostTotal, - } -) - -func RegisterStats() { - internal.RegisterCommonStats() - metrics.RegisterAll(janusMcuStats...) -} - -func UnregisterStats() { - internal.UnregisterCommonStats() - metrics.UnregisterAll(janusMcuStats...) -} diff --git a/sfu/janus/stream_selection_test.go b/sfu/janus/stream_selection_test.go deleted file mode 100644 index cc0ed1d..0000000 --- a/sfu/janus/stream_selection_test.go +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" -) - -func TestStreamSelection(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - testcases := []api.StringMap{ - {}, - { - "substream": 1.0, - }, - { - "temporal": 1.0, - }, - { - "substream": float32(1.0), - }, - { - "temporal": float32(1.0), - }, - { - "substream": 1, - "temporal": 3, - }, - { - "substream": 1, - "audio": true, - "video": false, - }, - } - - for idx, tc := range testcases { - parsed, err := parseStreamSelection(tc) - if assert.NoError(err, "failed for testcase %d: %+v", idx, tc) { - assert.Equal(len(tc) > 0, parsed.HasValues(), "failed for testcase %d: %+v", idx, tc) - m := make(api.StringMap) - parsed.AddToMessage(m) - for k, v := range tc { - assert.EqualValues(v, m[k], "failed for key %s in testcase %d", k, idx) - } - } - } - - _, err := parseStreamSelection(api.StringMap{ - "substream": "foo", - }) - assert.ErrorContains(err, "unsupported substream value") - - _, err = parseStreamSelection(api.StringMap{ - "temporal": "foo", - }) - assert.ErrorContains(err, "unsupported temporal value") - - _, err = parseStreamSelection(api.StringMap{ - "audio": 1, - }) - assert.ErrorContains(err, "unsupported audio value") - - _, err = parseStreamSelection(api.StringMap{ - "video": "true", - }) - assert.ErrorContains(err, "unsupported video value") -} diff --git a/sfu/janus/subscriber.go b/sfu/janus/subscriber.go deleted file mode 100644 index 0d3bd73..0000000 --- a/sfu/janus/subscriber.go +++ /dev/null @@ -1,384 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package janus - -import ( - "context" - "fmt" - "strconv" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - sfuinternal "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" -) - -type janusSubscriber struct { - janusClient - - publisher api.PublicSessionId -} - -func (p *janusSubscriber) JanusHandle() *janus.Handle { - return p.handle.Load() -} - -func (p *janusSubscriber) Publisher() api.PublicSessionId { - return p.publisher -} - -func (p *janusSubscriber) handleEvent(event *janus.EventMsg) { - if videoroom := getPluginStringValue(event.Plugindata, pluginVideoRoom, "videoroom"); videoroom != "" { - ctx := context.TODO() - switch videoroom { - case "destroyed": - p.logger.Printf("Subscriber %d: associated room has been destroyed, closing", p.handleId.Load()) - go p.Close(ctx) - case "updated": - streams, ok := getPluginValue(event.Plugindata, pluginVideoRoom, "streams").([]any) - if !ok || len(streams) == 0 { - // The streams list will be empty if no stream was changed. - return - } - - for _, stream := range streams { - if stream, ok := api.ConvertStringMap(stream); ok { - if (stream["type"] == "audio" || stream["type"] == "video") && stream["active"] != false { - return - } - } - } - - p.logger.Printf("Subscriber %d: received updated event with no active media streams, closing", p.handleId.Load()) - go p.Close(ctx) - case "event": - // Handle renegotiations, but ignore other events like selected - // substream / temporal layer. - if getPluginStringValue(event.Plugindata, pluginVideoRoom, "configured") == "ok" && - event.Jsep != nil && event.Jsep["type"] == "offer" && event.Jsep["sdp"] != nil { - p.logger.Printf("Subscriber %d: received updated offer", p.handleId.Load()) - p.listener.OnUpdateOffer(p, event.Jsep) - } else { - p.logger.Printf("Subscriber %d: received unsupported event %+v", p.handleId.Load(), event) - } - case "slow_link": - // Ignore, processed through "handleSlowLink" in the general events. - default: - p.logger.Printf("Unsupported videoroom event %s for subscriber %d: %+v", videoroom, p.handleId.Load(), event) - } - } else { - p.logger.Printf("Unsupported event for subscriber %d: %+v", p.handleId.Load(), event) - } -} - -func (p *janusSubscriber) handleHangup(event *janus.HangupMsg) { - p.logger.Printf("Subscriber %d received hangup (%s), closing", p.handleId.Load(), event.Reason) - go p.Close(context.Background()) -} - -func (p *janusSubscriber) handleDetached(event *janus.DetachedMsg) { - p.logger.Printf("Subscriber %d received detached, closing", p.handleId.Load()) - go p.Close(context.Background()) -} - -func (p *janusSubscriber) handleConnected(event *janus.WebRTCUpMsg) { - p.logger.Printf("Subscriber %d received connected", p.handleId.Load()) - p.mcu.SubscriberConnected(p.Id(), p.publisher, p.streamType) -} - -func (p *janusSubscriber) handleSlowLink(event *janus.SlowLinkMsg) { - if event.Uplink { - p.logger.Printf("Subscriber %s (%d) is reporting %d lost packets on the uplink (Janus -> client)", p.listener.PublicId(), p.handleId.Load(), event.Lost) - } else { - p.logger.Printf("Subscriber %s (%d) is reporting %d lost packets on the downlink (client -> Janus)", p.listener.PublicId(), p.handleId.Load(), event.Lost) - } -} - -func (p *janusSubscriber) handleMedia(event *janus.MediaMsg) { - // Only triggered for publishers -} - -func (p *janusSubscriber) NotifyReconnected() { - ctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) - defer cancel() - handle, pub, err := p.mcu.getOrCreateSubscriberHandle(ctx, p.publisher, p.streamType) - if err != nil { - // TODO(jojo): Retry? - p.logger.Printf("Could not reconnect subscriber for publisher %s: %s", p.publisher, err) - p.Close(context.Background()) - return - } - - if prev := p.handle.Swap(handle); prev != nil { - if _, err := prev.Detach(context.Background()); err != nil { - p.logger.Printf("Error detaching old subscriber handle %d: %s", prev.Id, err) - } - } - p.handleId.Store(handle.Id) - p.roomId = pub.roomId - p.sid = strconv.FormatUint(handle.Id, 10) - p.listener.SubscriberSidUpdated(p) - p.logger.Printf("Subscriber %d for publisher %s reconnected on handle %d", p.id, p.publisher, p.handleId.Load()) -} - -func (p *janusSubscriber) closeClient(ctx context.Context) bool { - if !p.janusClient.closeClient(ctx) { - return false - } - - p.mcu.stats.DecSubscriber(p.streamType) - return true -} - -func (p *janusSubscriber) Close(ctx context.Context) { - p.mu.Lock() - closed := p.closeClient(ctx) - p.mu.Unlock() - - if closed { - p.mcu.SubscriberDisconnected(p.Id(), p.publisher, p.streamType) - } - p.mcu.unregisterClient(p) - p.listener.SubscriberClosed(p) - p.janusClient.Close(ctx) -} - -func (p *janusSubscriber) joinRoom(ctx context.Context, stream *streamSelection, callback func(error, api.StringMap)) { - handle := p.handle.Load() - if handle == nil { - callback(sfu.ErrNotConnected, nil) - return - } - - waiter, stop := p.mcu.newPublisherConnectedWaiter(p.publisher, p.streamType) - defer stop() - - loggedNotPublishingYet := false -retry: - join_msg := api.StringMap{ - "request": "join", - "ptype": "subscriber", - "room": p.roomId, - } - if p.mcu.isMultistream() { - join_msg["streams"] = []api.StringMap{ - { - "feed": streamTypeUserIds[p.streamType], - }, - } - } else { - join_msg["feed"] = streamTypeUserIds[p.streamType] - } - if stream != nil { - stream.AddToMessage(join_msg) - } - join_response, err := handle.Message(ctx, join_msg, nil) - if err != nil { - callback(err, nil) - return - } - - if error_code := getPluginIntValue(p.logger, join_response.Plugindata, pluginVideoRoom, "error_code"); error_code > 0 { - switch error_code { - case janus.JANUS_VIDEOROOM_ERROR_ALREADY_JOINED: - // The subscriber is already connected to the room. This can happen - // if a client leaves a call but keeps the subscriber objects active. - // On joining the call again, the subscriber tries to join on the - // MCU which will fail because he is still connected. - // To get a new Offer SDP, we have to tear down the session on the - // MCU and join again. - p.mu.Lock() - p.closeClient(ctx) - p.mu.Unlock() - - var pub *janusPublisher - handle, pub, err = p.mcu.getOrCreateSubscriberHandle(ctx, p.publisher, p.streamType) - if err != nil { - // Reconnection didn't work, need to unregister/remove subscriber - // so a new object will be created if the request is retried. - p.mcu.unregisterClient(p) - p.listener.SubscriberClosed(p) - callback(fmt.Errorf("already connected as subscriber for %s, error during re-joining: %s", p.streamType, err), nil) - return - } - - if prev := p.handle.Swap(handle); prev != nil { - if _, err := prev.Detach(context.Background()); err != nil { - p.logger.Printf("Error detaching old subscriber handle %d: %s", prev.Id, err) - } - } - p.handleId.Store(handle.Id) - p.roomId = pub.roomId - p.sid = strconv.FormatUint(handle.Id, 10) - p.listener.SubscriberSidUpdated(p) - p.closeChan = make(chan struct{}, 1) - p.mcu.stats.IncSubscriber(p.streamType) - go p.run(handle, p.closeChan) - p.logger.Printf("Already connected subscriber %d for %s, leaving and re-joining on handle %d", p.id, p.streamType, p.handleId.Load()) - goto retry - case janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM: - fallthrough - case janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED: - switch error_code { - case janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM: - p.logger.Printf("Publisher %s not created yet for %s, not joining room %d as subscriber", p.publisher, p.streamType, p.roomId) - p.Close(context.Background()) - callback(fmt.Errorf("Publisher %s not created yet for %s", p.publisher, p.streamType), nil) - return - case janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED: - p.logger.Printf("Publisher %s not sending yet for %s, wait and retry to join room %d as subscriber", p.publisher, p.streamType, p.roomId) - } - - if !loggedNotPublishingYet { - loggedNotPublishingYet = true - sfuinternal.StatsWaitingForPublisherTotal.WithLabelValues(string(p.streamType)).Inc() - } - - if err := waiter.Wait(ctx); err != nil { - p.Close(context.Background()) - callback(err, nil) - return - } - p.logger.Printf("Retry subscribing %s from %s", p.streamType, p.publisher) - goto retry - default: - // TODO(jojo): Should we handle other errors, too? - callback(fmt.Errorf("error joining room as subscriber: %+v", join_response), nil) - return - } - } - // p.logger.Println("Joined as listener", join_response) - - p.session = join_response.Session - callback(nil, join_response.Jsep) -} - -func (p *janusSubscriber) update(ctx context.Context, stream *streamSelection, callback func(error, api.StringMap)) { - handle := p.handle.Load() - if handle == nil { - callback(sfu.ErrNotConnected, nil) - return - } - - configure_msg := api.StringMap{ - "request": "configure", - "update": true, - } - if stream != nil { - stream.AddToMessage(configure_msg) - } - configure_response, err := handle.Message(ctx, configure_msg, nil) - if err != nil { - callback(err, nil) - return - } - - callback(nil, configure_response.Jsep) -} - -func (p *janusSubscriber) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { - sfuinternal.StatsMcuMessagesTotal.WithLabelValues(data.Type).Inc() - jsep_msg := data.Payload - switch data.Type { - case "requestoffer": - fallthrough - case "sendoffer": - p.deferred <- func() { - msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) - defer cancel() - - stream, err := parseStreamSelection(jsep_msg) - if err != nil { - go callback(err, nil) - return - } - - if data.Sid == "" || data.Sid != p.Sid() { - p.joinRoom(msgctx, stream, callback) - } else { - p.update(msgctx, stream, callback) - } - } - case "answer": - p.deferred <- func() { - if api.FilterSDPCandidates(data.AnswerSdp, p.mcu.settings.allowedCandidates.Load(), p.mcu.settings.blockedCandidates.Load()) { - // Update request with filtered SDP. - marshalled, err := data.AnswerSdp.Marshal() - if err != nil { - go callback(fmt.Errorf("could not marshal filtered answer: %w", err), nil) - return - } - - jsep_msg["sdp"] = string(marshalled) - } - - msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) - defer cancel() - - if data.Sid == "" || data.Sid == p.Sid() { - p.sendAnswer(msgctx, jsep_msg, callback) - } else { - go callback(fmt.Errorf("answer message sid (%s) does not match subscriber sid (%s)", data.Sid, p.Sid()), nil) - } - } - case "candidate": - if api.FilterCandidate(data.Candidate, p.mcu.settings.allowedCandidates.Load(), p.mcu.settings.blockedCandidates.Load()) { - go callback(api.ErrCandidateFiltered, nil) - return - } - - p.deferred <- func() { - msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) - defer cancel() - - if data.Sid == "" || data.Sid == p.Sid() { - p.sendCandidate(msgctx, jsep_msg["candidate"], callback) - } else { - go callback(fmt.Errorf("candidate message sid (%s) does not match subscriber sid (%s)", data.Sid, p.Sid()), nil) - } - } - case "endOfCandidates": - // Ignore - case "selectStream": - stream, err := parseStreamSelection(jsep_msg) - if err != nil { - go callback(err, nil) - return - } - - if stream == nil || !stream.HasValues() { - // Nothing to do - go callback(nil, nil) - return - } - - p.deferred <- func() { - msgctx, cancel := context.WithTimeout(context.Background(), p.mcu.settings.Timeout()) - defer cancel() - - p.selectStream(msgctx, stream, callback) - } - default: - // Return error asynchronously - go callback(fmt.Errorf("unsupported message type: %s", data.Type), nil) - } -} diff --git a/sfu/janus/test/janus.go b/sfu/janus/test/janus.go deleted file mode 100644 index 319ea23..0000000 --- a/sfu/janus/test/janus.go +++ /dev/null @@ -1,588 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "encoding/json" - "maps" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/janus/janus" -) - -const ( - pluginVideoRoom = "janus.plugin.videoroom" - eventWebsocket = "janus.eventhandler.wsevh" -) - -type JanusHandle struct { - id uint64 - - sdp atomic.Value -} - -type JanusRoom struct { - id uint64 - - publisher atomic.Pointer[JanusHandle] -} - -func (r *JanusRoom) Id() uint64 { - return r.id -} - -type JanusHandler func(room *JanusRoom, body api.StringMap, jsep api.StringMap) (any, *janus.ErrorMsg) - -type JanusGateway struct { - t *testing.T - - sid atomic.Uint64 - tid atomic.Uint64 - hid atomic.Uint64 // +checklocksignore: Atomic - rid atomic.Uint64 // +checklocksignore: Atomic - mu sync.Mutex - - // +checklocks:mu - sessions map[uint64]*janus.Session - // +checklocks:mu - transactions map[uint64]*janus.Transaction - // +checklocks:mu - handles map[uint64]*JanusHandle - // +checklocks:mu - rooms map[uint64]*JanusRoom - // +checklocks:mu - handlers map[string]JanusHandler - - // +checklocks:mu - attachCount int - // +checklocks:mu - joinCount int - - // +checklocks:mu - handleRooms map[*JanusHandle]*JanusRoom -} - -func NewJanusGateway(t *testing.T) *JanusGateway { - gateway := &JanusGateway{ - t: t, - - sessions: make(map[uint64]*janus.Session), - transactions: make(map[uint64]*janus.Transaction), - handles: make(map[uint64]*JanusHandle), - rooms: make(map[uint64]*JanusRoom), - handlers: make(map[string]JanusHandler), - - handleRooms: make(map[*JanusHandle]*JanusRoom), - } - - t.Cleanup(func() { - assert := assert.New(t) - gateway.mu.Lock() - defer gateway.mu.Unlock() - assert.Empty(gateway.sessions) - assert.Empty(gateway.transactions) - assert.Empty(gateway.handles) - assert.Empty(gateway.rooms) - assert.Empty(gateway.handleRooms) - }) - - return gateway -} - -func (g *JanusGateway) RegisterHandlers(handlers map[string]JanusHandler) { - g.mu.Lock() - defer g.mu.Unlock() - maps.Copy(g.handlers, handlers) -} - -func (g *JanusGateway) Info(ctx context.Context) (*janus.InfoMsg, error) { - return &janus.InfoMsg{ - Name: "TestJanus", - Version: 1400, - VersionString: "1.4.0", - Author: "struktur AG", - DataChannels: true, - EventHandlers: true, - FullTrickle: true, - Plugins: map[string]janus.PluginInfo{ - pluginVideoRoom: { - Name: "Test VideoRoom plugin", - VersionString: "0.0.0", - Author: "struktur AG", - }, - }, - Events: map[string]janus.PluginInfo{ - eventWebsocket: { - Name: "Test Websocket events", - VersionString: "0.0.0", - Author: "struktur AG", - }, - }, - }, nil -} - -func (g *JanusGateway) Create(ctx context.Context) (*janus.Session, error) { - sid := g.sid.Add(1) - session := janus.NewSession(sid, g) - g.mu.Lock() - defer g.mu.Unlock() - g.sessions[sid] = session - return session, nil -} - -func (g *JanusGateway) Close() error { - return nil -} - -func (g *JanusGateway) simulateEvent(delay time.Duration, session *janus.Session, handle *JanusHandle, event any) { - go func() { - time.Sleep(delay) - session.Lock() - h, found := session.Handles[handle.id] - session.Unlock() - if found { - h.Events <- event - } - }() -} - -// +checklocks:g.mu -func (g *JanusGateway) processMessage(session *janus.Session, handle *JanusHandle, body api.StringMap, jsep api.StringMap) any { - request := body["request"].(string) - switch request { - case "create": - room := &JanusRoom{ - id: g.rid.Add(1), - } - g.rooms[room.id] = room - - return &janus.SuccessMsg{ - PluginData: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "room": room.id, - }, - }, - } - case "join": - rid := body["room"].(float64) - room := g.rooms[uint64(rid)] - error_code := janus.JANUS_OK - if body["ptype"] == "subscriber" { - if strings.Contains(g.t.Name(), "NoSuchRoom") { - g.joinCount++ - if g.joinCount == 1 { - error_code = janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM - } - } else if strings.Contains(g.t.Name(), "AlreadyJoined") { - g.joinCount++ - if g.joinCount == 1 { - error_code = janus.JANUS_VIDEOROOM_ERROR_ALREADY_JOINED - } - } else if strings.Contains(g.t.Name(), "SubscriberTimeout") { - g.joinCount++ - if g.joinCount == 1 { - error_code = janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED - } - } - } - if error_code != janus.JANUS_OK { - return &janus.EventMsg{ - Plugindata: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "error_code": error_code, - }, - }, - } - } - - if room == nil { - return &janus.EventMsg{ - Plugindata: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "error_code": janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, - }, - }, - } - } - - g.handleRooms[handle] = room - switch body["ptype"] { - case "publisher": - if !assert.True(g.t, room.publisher.CompareAndSwap(nil, handle)) { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_VIDEOROOM_ERROR_ALREADY_PUBLISHED, - Reason: "Already publisher in this room", - }, - } - } - - return &janus.EventMsg{ - Session: session.Id, - Handle: handle.id, - Plugindata: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "room": room.id, - }, - }, - } - case "subscriber": - publisher := room.publisher.Load() - if publisher == nil || publisher.sdp.Load() == nil { - return &janus.EventMsg{ - Plugindata: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "error_code": janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED, - }, - }, - } - } - - sdp := publisher.sdp.Load() - - // Simulate "connected" event for subscriber. - g.simulateEvent(15*time.Millisecond, session, handle, &janus.WebRTCUpMsg{ - Session: session.Id, - Handle: handle.id, - }) - - if strings.Contains(g.t.Name(), "CloseEmptyStreams") { - // Simulate stream update event with no active streams. - g.simulateEvent(20*time.Millisecond, session, handle, &janus.EventMsg{ - Session: session.Id, - Handle: handle.id, - Plugindata: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "videoroom": "updated", - "streams": []any{ - api.StringMap{ - "type": "audio", - "active": false, - }, - }, - }, - }, - }) - } - - if strings.Contains(g.t.Name(), "SubscriberRoomDestroyed") { - // Simulate event that subscriber room has been destroyed. - g.simulateEvent(20*time.Millisecond, session, handle, &janus.EventMsg{ - Session: session.Id, - Handle: handle.id, - Plugindata: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "videoroom": "destroyed", - }, - }, - }) - } - - if strings.Contains(g.t.Name(), "SubscriberUpdateOffer") { - // Simulate event that subscriber receives new offer. - g.simulateEvent(20*time.Millisecond, session, handle, &janus.EventMsg{ - Session: session.Id, - Handle: handle.id, - Plugindata: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{ - "videoroom": "event", - "configured": "ok", - }, - }, - Jsep: map[string]any{ - "type": "offer", - "sdp": mock.MockSdpOfferAudioOnly, - }, - }) - } - - return &janus.EventMsg{ - Jsep: api.StringMap{ - "type": "offer", - "sdp": sdp.(string), - }, - } - } - case "destroy": - rid := body["room"].(float64) - room := g.rooms[uint64(rid)] - if room == nil { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, - Reason: "Room not found", - }, - } - } - - assert.Equal(g.t, room.id, uint64(rid)) - delete(g.rooms, uint64(rid)) - for h, r := range g.handleRooms { - if r.id == room.id { - delete(g.handleRooms, h) - } - } - - return &janus.SuccessMsg{ - PluginData: janus.PluginData{ - Plugin: pluginVideoRoom, - Data: api.StringMap{}, - }, - } - default: - var room *JanusRoom - if roomId, found := body["room"]; found { - rid := roomId.(float64) - if room = g.rooms[uint64(rid)]; room == nil { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_VIDEOROOM_ERROR_NO_SUCH_ROOM, - Reason: "Room not found", - }, - } - } - } else { - if room, found = g.handleRooms[handle]; !found { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, - Reason: "No joined to a room yet.", - }, - } - } - } - - handler, found := g.handlers[request] - if found { - var err *janus.ErrorMsg - result, err := handler(room, body, jsep) - if err != nil { - result = err - } else { - switch request { - case "start": - g.handleRooms[handle] = room - case "configure": - if sdp, found := jsep["sdp"]; found { - handle.sdp.Store(sdp.(string)) - if !strings.Contains(g.t.Name(), "SubscriberTimeout") { - // Simulate "connected" event for publisher. - g.simulateEvent(10*time.Millisecond, session, handle, &janus.WebRTCUpMsg{ - Session: session.Id, - Handle: handle.id, - }) - } - } - } - } - return result - } - } - - return nil -} - -func (g *JanusGateway) processRequest(msg api.StringMap) any { - method, found := msg["janus"] - if !found { - return nil - } - - sid := msg["session_id"].(float64) - g.mu.Lock() - defer g.mu.Unlock() - session := g.sessions[uint64(sid)] - if session == nil { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_ERROR_SESSION_NOT_FOUND, - Reason: "Session not found", - }, - } - } - - switch method { - case "attach": - if strings.Contains(g.t.Name(), "AlreadyJoinedAttachError") { - g.attachCount++ - if g.attachCount == 4 { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_ERROR_UNKNOWN, - Reason: "Fail for test", - }, - } - } - } - - handle := &JanusHandle{ - id: g.hid.Add(1), - } - - g.handles[handle.id] = handle - - return &janus.SuccessMsg{ - Data: janus.SuccessData{ - ID: handle.id, - }, - } - case "detach": - hid := msg["handle_id"].(float64) - handle, found := g.handles[uint64(hid)] - if found { - delete(g.handles, handle.id) - } - if handle == nil { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_ERROR_HANDLE_NOT_FOUND, - Reason: "Handle not found", - }, - } - } - - return &janus.AckMsg{} - case "destroy": - delete(g.sessions, session.Id) - return &janus.AckMsg{} - case "message", "trickle": - hid := msg["handle_id"].(float64) - handle, found := g.handles[uint64(hid)] - if !found { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_ERROR_HANDLE_NOT_FOUND, - Reason: "Handle not found", - }, - } - } - - var result any - switch method { - case "message": - body, ok := api.ConvertStringMap(msg["body"]) - assert.True(g.t, ok, "not a string map: %+v", msg["body"]) - if jsepOb, found := msg["jsep"]; found { - if jsep, ok := api.ConvertStringMap(jsepOb); assert.True(g.t, ok, "not a string map: %+v", jsepOb) { - result = g.processMessage(session, handle, body, jsep) - } - } else { - result = g.processMessage(session, handle, body, nil) - } - case "trickle": - room, found := g.handleRooms[handle] - if !found { - return &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_VIDEOROOM_ERROR_INVALID_REQUEST, - Reason: "No joined to a room yet.", - }, - } - } - - handler, found := g.handlers[method.(string)] - if found { - var err *janus.ErrorMsg - result, err = handler(room, msg, nil) - if err != nil { - result = err - } - } - } - - if ev, ok := result.(*janus.EventMsg); ok { - if ev.Session == 0 { - ev.Session = uint64(sid) - } - if ev.Handle == 0 { - ev.Handle = handle.id - } - } - return result - } - - return nil -} - -func (g *JanusGateway) Send(msg api.StringMap, t *janus.Transaction) (uint64, error) { - tid := g.tid.Add(1) - - data, err := json.Marshal(msg) - require.NoError(g.t, err) - err = json.Unmarshal(data, &msg) - require.NoError(g.t, err) - - go t.Run() - - g.mu.Lock() - defer g.mu.Unlock() - g.transactions[tid] = t - - go func() { - result := g.processRequest(msg) - if !assert.NotNil(g.t, result, "Unsupported request %+v", msg) { - result = &janus.ErrorMsg{ - Err: janus.ErrorData{ - Code: janus.JANUS_ERROR_UNKNOWN, - Reason: "Not implemented", - }, - } - } - - t.Add(result) - }() - - return tid, nil -} - -func (g *JanusGateway) RemoveTransaction(id uint64) { - g.mu.Lock() - defer g.mu.Unlock() - if t, found := g.transactions[id]; found { - delete(g.transactions, id) - t.Quit() - } -} - -func (g *JanusGateway) RemoveSession(session *janus.Session) { - g.mu.Lock() - defer g.mu.Unlock() - delete(g.sessions, session.Id) -} diff --git a/sfu/mock/mock.go b/sfu/mock/mock.go deleted file mode 100644 index 0f47340..0000000 --- a/sfu/mock/mock.go +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package mock - -import ( - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" -) - -type Listener struct { - publicId api.PublicSessionId -} - -func NewListener(publicId api.PublicSessionId) *Listener { - return &Listener{ - publicId: publicId, - } -} - -func (m *Listener) PublicId() api.PublicSessionId { - return m.publicId -} - -func (m *Listener) OnUpdateOffer(client sfu.Client, offer api.StringMap) { - -} - -func (m *Listener) OnIceCandidate(client sfu.Client, candidate any) { - -} - -func (m *Listener) OnIceCompleted(client sfu.Client) { - -} - -func (m *Listener) SubscriberSidUpdated(subscriber sfu.Subscriber) { - -} - -func (m *Listener) PublisherClosed(publisher sfu.Publisher) { - -} - -func (m *Listener) SubscriberClosed(subscriber sfu.Subscriber) { - -} - -type Initiator struct { - country geoip.Country -} - -func NewInitiator(country geoip.Country) *Initiator { - return &Initiator{ - country: country, - } -} - -func (m *Initiator) Country() geoip.Country { - return m.country -} diff --git a/sfu/proxy/proxy.go b/sfu/proxy/proxy.go deleted file mode 100644 index 52278c9..0000000 --- a/sfu/proxy/proxy.go +++ /dev/null @@ -1,2463 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2020 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package proxy - -import ( - "context" - "crypto/rsa" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "iter" - "math/rand/v2" - "net" - "net/http" - "net/url" - "os" - "slices" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/dlintw/goconf" - "github.com/golang-jwt/jwt/v5" - "github.com/gorilla/websocket" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/dns" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - sfuinternal "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -const ( - closeTimeout = time.Second - - proxyDebugMessages = false - - // Very high value so the connections get sorted at the end. - loadNotConnected = 1000000 - - // Sort connections by load every 10 publishing requests or once per second. - connectionSortRequests = 10 - connectionSortInterval = time.Second - - proxyUrlTypeStatic = "static" - proxyUrlTypeEtcd = "etcd" - - initialReconnectInterval = 1 * time.Second - maxReconnectInterval = 16 * time.Second - - // Time allowed to write a message to the peer. - writeWait = 10 * time.Second - - // Time allowed to read the next pong message from the peer. - pongWait = 60 * time.Second - - // Send pings to peer with this period. Must be less than pongWait. - pingPeriod = (pongWait * 9) / 10 - - defaultProxyTimeoutSeconds = 2 - - rttLogDuration = 500 * time.Millisecond -) - -type ContinentsMap map[geoip.Continent][]geoip.Continent - -type McuProxy interface { - AddConnection(ignoreErrors bool, url string, ips ...net.IP) error - KeepConnection(url string, ips ...net.IP) - RemoveConnection(url string, ips ...net.IP) -} - -type proxyPubSubCommon struct { - logger log.Logger - - sid string - streamType sfu.StreamType - maxBitrate api.Bandwidth - proxyId string - conn *proxyConnection - listener sfu.Listener -} - -func (c *proxyPubSubCommon) Id() string { - return c.proxyId -} - -func (c *proxyPubSubCommon) Sid() string { - return c.sid -} - -func (c *proxyPubSubCommon) StreamType() sfu.StreamType { - return c.streamType -} - -func (c *proxyPubSubCommon) MaxBitrate() api.Bandwidth { - return c.maxBitrate -} - -func (c *proxyPubSubCommon) doSendMessage(ctx context.Context, msg *proxy.ClientMessage, callback func(error, api.StringMap)) { - c.conn.performAsyncRequest(ctx, msg, func(err error, response *proxy.ServerMessage) { - if err != nil { - callback(err, nil) - return - } - - if proxyDebugMessages { - c.logger.Printf("Response from %s: %+v", c.conn, response) - } - if response.Type == "error" { - callback(response.Error, nil) - } else if response.Payload != nil { - callback(nil, response.Payload.Payload) - } else { - callback(nil, nil) - } - }) -} - -func (c *proxyPubSubCommon) doProcessPayload(client sfu.Client, msg *proxy.PayloadServerMessage) { - switch msg.Type { - case "offer": - offer, ok := api.ConvertStringMap(msg.Payload["offer"]) - if !ok { - c.logger.Printf("Unsupported payload from %s: %+v", c.conn, msg) - return - } - - c.listener.OnUpdateOffer(client, offer) - case "candidate": - c.listener.OnIceCandidate(client, msg.Payload["candidate"]) - default: - c.logger.Printf("Unsupported payload from %s: %+v", c.conn, msg) - } -} - -type proxyPublisher struct { - proxyPubSubCommon - - id api.PublicSessionId - settings sfu.NewPublisherSettings -} - -func newProxyPublisher(logger log.Logger, id api.PublicSessionId, sid string, streamType sfu.StreamType, maxBitrate api.Bandwidth, settings sfu.NewPublisherSettings, proxyId string, conn *proxyConnection, listener sfu.Listener) *proxyPublisher { - return &proxyPublisher{ - proxyPubSubCommon: proxyPubSubCommon{ - logger: logger, - - sid: sid, - streamType: streamType, - maxBitrate: maxBitrate, - proxyId: proxyId, - conn: conn, - listener: listener, - }, - id: id, - settings: settings, - } -} - -func (p *proxyPublisher) GetConnectionURL() (string, net.IP) { - return p.conn.rawUrl, p.conn.ip -} - -func (p *proxyPublisher) PublisherId() api.PublicSessionId { - return p.id -} - -func (p *proxyPublisher) HasMedia(mt sfu.MediaType) bool { - return (p.settings.MediaTypes & mt) == mt -} - -func (p *proxyPublisher) SetMedia(mt sfu.MediaType) { - // TODO: Also update mediaTypes on proxy. - p.settings.MediaTypes = mt -} - -func (p *proxyPublisher) NotifyClosed() { - p.logger.Printf("Publisher %s at %s was closed", p.proxyId, p.conn) - p.listener.PublisherClosed(p) - p.conn.removePublisher(p) -} - -func (p *proxyPublisher) Close(ctx context.Context) { - p.NotifyClosed() - - msg := &proxy.ClientMessage{ - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "delete-publisher", - ClientId: p.proxyId, - }, - } - - if response, _, err := p.conn.performSyncRequest(ctx, msg); err != nil { - p.logger.Printf("Could not delete publisher %s at %s: %s", p.proxyId, p.conn, err) - return - } else if response.Type == "error" { - p.logger.Printf("Could not delete publisher %s at %s: %s", p.proxyId, p.conn, response.Error) - return - } - - p.logger.Printf("Deleted publisher %s at %s", p.proxyId, p.conn) -} - -func (p *proxyPublisher) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { - msg := &proxy.ClientMessage{ - Type: "payload", - Payload: &proxy.PayloadClientMessage{ - Type: data.Type, - ClientId: p.proxyId, - Sid: data.Sid, - Payload: data.Payload, - }, - } - - p.doSendMessage(ctx, msg, callback) -} - -func (p *proxyPublisher) ProcessPayload(msg *proxy.PayloadServerMessage) { - p.doProcessPayload(p, msg) -} - -func (p *proxyPublisher) ProcessEvent(msg *proxy.EventServerMessage) { - switch msg.Type { - case "ice-completed": - p.listener.OnIceCompleted(p) - case "publisher-closed": - p.NotifyClosed() - default: - p.logger.Printf("Unsupported event from %s: %+v", p.conn, msg) - } -} - -type proxySubscriber struct { - proxyPubSubCommon - - publisherId api.PublicSessionId - publisherConn *proxyConnection -} - -func newProxySubscriber(logger log.Logger, publisherId api.PublicSessionId, sid string, streamType sfu.StreamType, maxBitrate api.Bandwidth, proxyId string, conn *proxyConnection, listener sfu.Listener, publisherConn *proxyConnection) *proxySubscriber { - return &proxySubscriber{ - proxyPubSubCommon: proxyPubSubCommon{ - logger: logger, - - sid: sid, - streamType: streamType, - maxBitrate: maxBitrate, - proxyId: proxyId, - conn: conn, - listener: listener, - }, - - publisherId: publisherId, - publisherConn: publisherConn, - } -} - -func (s *proxySubscriber) GetConnectionURL() (string, net.IP) { - return s.conn.rawUrl, s.conn.ip -} - -func (s *proxySubscriber) Publisher() api.PublicSessionId { - return s.publisherId -} - -func (s *proxySubscriber) NotifyClosed() { - if s.publisherConn != nil { - s.logger.Printf("Remote subscriber %s at %s (forwarded to %s) was closed", s.proxyId, s.conn, s.publisherConn) - } else { - s.logger.Printf("Subscriber %s at %s was closed", s.proxyId, s.conn) - } - s.listener.SubscriberClosed(s) - s.conn.removeSubscriber(s) -} - -func (s *proxySubscriber) Close(ctx context.Context) { - s.NotifyClosed() - - msg := &proxy.ClientMessage{ - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "delete-subscriber", - ClientId: s.proxyId, - }, - } - - if response, _, err := s.conn.performSyncRequest(ctx, msg); err != nil { - if s.publisherConn != nil { - s.logger.Printf("Could not delete remote subscriber %s at %s (forwarded to %s): %s", s.proxyId, s.conn, s.publisherConn, err) - } else { - s.logger.Printf("Could not delete subscriber %s at %s: %s", s.proxyId, s.conn, err) - } - return - } else if response.Type == "error" { - if s.publisherConn != nil { - s.logger.Printf("Could not delete remote subscriber %s at %s (forwarded to %s): %s", s.proxyId, s.conn, s.publisherConn, response.Error) - } else { - s.logger.Printf("Could not delete subscriber %s at %s: %s", s.proxyId, s.conn, response.Error) - } - return - } - - if s.publisherConn != nil { - s.logger.Printf("Deleted remote subscriber %s at %s (forwarded to %s)", s.proxyId, s.conn, s.publisherConn) - } else { - s.logger.Printf("Deleted subscriber %s at %s", s.proxyId, s.conn) - } -} - -func (s *proxySubscriber) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { - msg := &proxy.ClientMessage{ - Type: "payload", - Payload: &proxy.PayloadClientMessage{ - Type: data.Type, - ClientId: s.proxyId, - Sid: data.Sid, - Payload: data.Payload, - }, - } - - s.doSendMessage(ctx, msg, callback) -} - -func (s *proxySubscriber) ProcessPayload(msg *proxy.PayloadServerMessage) { - s.doProcessPayload(s, msg) -} - -func (s *proxySubscriber) ProcessEvent(msg *proxy.EventServerMessage) { - switch msg.Type { - case "ice-completed": - s.listener.OnIceCompleted(s) - case "subscriber-sid-updated": - s.sid = msg.Sid - s.listener.SubscriberSidUpdated(s) - case "subscriber-closed": - s.NotifyClosed() - default: - s.logger.Printf("Unsupported event from %s: %+v", s.conn, msg) - } -} - -type mcuProxyCallback func(response *proxy.ServerMessage) - -type proxyConnection struct { - logger log.Logger - proxy *proxySFU - rawUrl string - url *url.URL - ip net.IP - connectToken string - - load atomic.Uint64 - bandwidth atomic.Pointer[proxy.EventServerBandwidth] - mu sync.Mutex - closer *internal.Closer - closedDone *internal.Closer - closed atomic.Bool - // +checklocks:mu - conn *websocket.Conn - - // +checklocks:mu - helloProcessed bool - connectedSince atomic.Int64 - reconnectTimer *time.Timer - reconnectInterval atomic.Int64 - shutdownScheduled atomic.Bool - closeScheduled atomic.Bool - trackClose atomic.Bool - temporary atomic.Bool - - connectedCond sync.Cond - - msgId atomic.Int64 - helloMsgId string - sessionId atomic.Value - country atomic.Value - version atomic.Value - features atomic.Value - - // +checklocks:mu - callbacks map[string]mcuProxyCallback - // +checklocks:mu - deferredCallbacks map[string]mcuProxyCallback - - publishersLock sync.RWMutex - // +checklocks:publishersLock - publishers map[string]*proxyPublisher - // +checklocks:publishersLock - publisherIds map[sfu.StreamId]api.PublicSessionId - - subscribersLock sync.RWMutex - // +checklocks:subscribersLock - subscribers map[string]*proxySubscriber -} - -func newProxyConnection(proxy *proxySFU, baseUrl string, ip net.IP, token string) (*proxyConnection, error) { - parsed, err := url.Parse(baseUrl) - if err != nil { - return nil, err - } - - conn := &proxyConnection{ - logger: proxy.logger, - proxy: proxy, - rawUrl: baseUrl, - url: parsed, - ip: ip, - connectToken: token, - closer: internal.NewCloser(), - closedDone: internal.NewCloser(), - callbacks: make(map[string]mcuProxyCallback), - publishers: make(map[string]*proxyPublisher), - publisherIds: make(map[sfu.StreamId]api.PublicSessionId), - subscribers: make(map[string]*proxySubscriber), - } - conn.connectedCond.L = &conn.mu - conn.reconnectInterval.Store(int64(initialReconnectInterval)) - conn.load.Store(loadNotConnected) - conn.bandwidth.Store(nil) - conn.country.Store(geoip.Country("")) - conn.version.Store("") - conn.features.Store([]string{}) - statsProxyBackendLoadCurrent.WithLabelValues(conn.url.String()).Set(0) - statsProxyUsageCurrent.WithLabelValues(conn.url.String(), "incoming").Set(0) - statsProxyUsageCurrent.WithLabelValues(conn.url.String(), "outgoing").Set(0) - statsProxyBandwidthCurrent.WithLabelValues(conn.url.String(), "incoming").Set(0) - statsProxyBandwidthCurrent.WithLabelValues(conn.url.String(), "outgoing").Set(0) - return conn, nil -} - -func (c *proxyConnection) String() string { - if c.ip != nil { - return fmt.Sprintf("%s (%s)", c.rawUrl, c.ip) - } - - return c.rawUrl -} - -func (c *proxyConnection) IsSameCountry(initiator sfu.Initiator) bool { - if initiator == nil { - return true - } - - initiatorCountry := initiator.Country() - if initiatorCountry == "" { - return true - } - - connCountry := c.Country() - if connCountry == "" { - return true - } - - return initiatorCountry == connCountry -} - -func (c *proxyConnection) IsSameContinent(initiator sfu.Initiator) bool { - if initiator == nil { - return true - } - - initiatorCountry := initiator.Country() - if initiatorCountry == "" { - return true - } - - connCountry := c.Country() - if connCountry == "" { - return true - } - - initiatorContinents, found := geoip.ContinentMap[initiatorCountry] - if found { - m := c.proxy.getContinentsMap() - // Map continents to other continents (e.g. use Europe for Africa). - for _, continent := range initiatorContinents { - if toAdd, found := m[continent]; found { - initiatorContinents = append(initiatorContinents, toAdd...) - } - } - - } - connContinents := geoip.ContinentMap[connCountry] - return ContinentsOverlap(initiatorContinents, connContinents) -} - -type proxyConnectionStats struct { - Url string `json:"url"` - IP net.IP `json:"ip,omitempty"` - Connected bool `json:"connected"` - Publishers int64 `json:"publishers"` - Clients int64 `json:"clients"` - Load *uint64 `json:"load,omitempty"` - Shutdown *bool `json:"shutdown,omitempty"` - Temporary *bool `json:"temporary,omitempty"` - Uptime *time.Time `json:"uptime,omitempty"` -} - -func (c *proxyConnection) GetStats() *proxyConnectionStats { - result := &proxyConnectionStats{ - Url: c.url.String(), - IP: c.ip, - } - c.mu.Lock() - if c.conn != nil { - result.Connected = true - if since := c.connectedSince.Load(); since != 0 { - t := time.UnixMicro(since) - result.Uptime = &t - } - load := c.Load() - result.Load = &load - shutdown := c.IsShutdownScheduled() - result.Shutdown = &shutdown - temporary := c.IsTemporary() - result.Temporary = &temporary - } - c.mu.Unlock() - c.publishersLock.RLock() - result.Publishers = int64(len(c.publishers)) - c.publishersLock.RUnlock() - c.subscribersLock.RLock() - result.Clients = int64(len(c.subscribers)) - c.subscribersLock.RUnlock() - result.Clients += result.Publishers - return result -} - -func (c *proxyConnection) Load() uint64 { - return c.load.Load() -} - -func (c *proxyConnection) Bandwidth() *proxy.EventServerBandwidth { - return c.bandwidth.Load() -} - -func (c *proxyConnection) Country() geoip.Country { - return c.country.Load().(geoip.Country) -} - -func (c *proxyConnection) Version() string { - return c.version.Load().(string) -} - -func (c *proxyConnection) Features() []string { - return c.features.Load().([]string) -} - -func (c *proxyConnection) SessionId() api.PublicSessionId { - sid := c.sessionId.Load() - if sid == nil { - return "" - } - - return sid.(api.PublicSessionId) -} - -func (c *proxyConnection) IsConnected() bool { - c.mu.Lock() - defer c.mu.Unlock() - return c.conn != nil && c.helloProcessed && c.SessionId() != "" -} - -func (c *proxyConnection) IsTemporary() bool { - return c.temporary.Load() -} - -func (c *proxyConnection) setTemporary() { - c.temporary.Store(true) -} - -func (c *proxyConnection) clearTemporary() { - c.temporary.Store(false) -} - -func (c *proxyConnection) IsShutdownScheduled() bool { - return c.shutdownScheduled.Load() || c.closeScheduled.Load() -} - -func (c *proxyConnection) readPump() { - defer func() { - if !c.closed.Load() { - c.scheduleReconnect() - } else { - c.closedDone.Close() - } - }() - defer c.close() - defer func() { - c.load.Store(loadNotConnected) - c.bandwidth.Store(nil) - }() - - c.mu.Lock() - conn := c.conn - c.mu.Unlock() - - conn.SetPongHandler(func(msg string) error { - now := time.Now() - conn.SetReadDeadline(now.Add(pongWait)) // nolint - if msg == "" { - return nil - } - if ts, err := strconv.ParseInt(msg, 10, 64); err == nil { - rtt := now.Sub(time.Unix(0, ts)) - if rtt >= rttLogDuration { - rtt_ms := rtt.Nanoseconds() / time.Millisecond.Nanoseconds() - c.logger.Printf("Proxy at %s has RTT of %d ms (%s)", c, rtt_ms, rtt) - } - } - return nil - }) - - for { - conn.SetReadDeadline(time.Now().Add(pongWait)) // nolint - _, message, err := conn.ReadMessage() - if err != nil { - if errors.Is(err, websocket.ErrCloseSent) { - break - } else if _, ok := err.(*websocket.CloseError); !ok || websocket.IsUnexpectedCloseError(err, - websocket.CloseNormalClosure, - websocket.CloseGoingAway, - websocket.CloseNoStatusReceived) { - c.logger.Printf("Error reading from %s: %v", c, err) - } - break - } - - var msg proxy.ServerMessage - if err := json.Unmarshal(message, &msg); err != nil { - c.logger.Printf("Error unmarshaling %s from %s: %s", string(message), c, err) - continue - } - - c.processMessage(&msg) - } -} - -func (c *proxyConnection) sendPing() bool { - c.mu.Lock() - defer c.mu.Unlock() - if c.conn == nil { - return false - } - - now := time.Now() - msg := strconv.FormatInt(now.UnixNano(), 10) - c.conn.SetWriteDeadline(now.Add(writeWait)) // nolint - if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil { - c.logger.Printf("Could not send ping to proxy at %s: %v", c, err) - go c.scheduleReconnect() - return false - } - - return true -} - -func (c *proxyConnection) writePump() { - ticker := time.NewTicker(pingPeriod) - defer func() { - ticker.Stop() - }() - - c.reconnectTimer = time.NewTimer(0) - defer c.reconnectTimer.Stop() - for { - select { - case <-c.reconnectTimer.C: - c.reconnect() - case <-ticker.C: - c.sendPing() - case <-c.closer.C: - return - } - } -} - -func (c *proxyConnection) start() { - go c.writePump() -} - -func (c *proxyConnection) sendClose() error { - c.mu.Lock() - defer c.mu.Unlock() - - if c.conn == nil { - return sfu.ErrNotConnected - } - - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint - return c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) -} - -func (c *proxyConnection) stop(ctx context.Context) { - if !c.closed.CompareAndSwap(false, true) { - return - } - - c.closer.Close() - if err := c.sendClose(); err != nil { - if err != sfu.ErrNotConnected { - c.logger.Printf("Could not send close message to %s: %s", c, err) - } - c.close() - return - } - - select { - case <-c.closedDone.C: - case <-ctx.Done(): - if err := ctx.Err(); err != nil { - c.logger.Printf("Error waiting for connection to %s get closed: %s", c, err) - c.close() - } - } -} - -func (c *proxyConnection) close() { - c.mu.Lock() - defer c.mu.Unlock() - - c.helloProcessed = false - - if c.conn != nil { - c.conn.Close() - c.conn = nil - c.connectedSince.Store(0) - if c.trackClose.CompareAndSwap(true, false) { - statsConnectedProxyBackendsCurrent.WithLabelValues(string(c.Country())).Dec() - } - } -} - -func (c *proxyConnection) stopCloseIfEmpty() { - c.closeScheduled.Store(false) -} - -func (c *proxyConnection) closeIfEmpty() bool { - c.closeScheduled.Store(true) - - var total int64 - c.publishersLock.RLock() - total += int64(len(c.publishers)) - c.publishersLock.RUnlock() - c.subscribersLock.RLock() - total += int64(len(c.subscribers)) - c.subscribersLock.RUnlock() - if total > 0 { - // Connection will be closed once all clients have disconnected. - c.logger.Printf("Connection to %s is still used by %d clients, defer closing", c, total) - return false - } - - go func() { - ctx, cancel := context.WithTimeout(context.Background(), closeTimeout) - defer cancel() - - c.logger.Printf("All clients disconnected, closing connection to %s", c) - c.stop(ctx) - - statsProxyBackendLoadCurrent.DeleteLabelValues(c.url.String()) - statsProxyUsageCurrent.DeleteLabelValues(c.url.String(), "incoming") - statsProxyUsageCurrent.DeleteLabelValues(c.url.String(), "outgoing") - statsProxyBandwidthCurrent.DeleteLabelValues(c.url.String(), "incoming") - statsProxyBandwidthCurrent.DeleteLabelValues(c.url.String(), "outgoing") - - c.proxy.removeConnection(c) - }() - return true -} - -func (c *proxyConnection) scheduleReconnect() { - if err := c.sendClose(); err != nil && err != sfu.ErrNotConnected { - c.logger.Printf("Could not send close message to %s: %s", c, err) - } - c.close() - - interval := c.reconnectInterval.Load() - // Prevent all servers from reconnecting at the same time in case of an - // interrupted connection to the proxy or a restart. - jitter := rand.Int64N(interval) - (interval / 2) - c.reconnectTimer.Reset(time.Duration(interval + jitter)) - - interval = min(interval*2, int64(maxReconnectInterval)) - c.reconnectInterval.Store(interval) -} - -func (c *proxyConnection) reconnect() { - u, err := c.url.Parse("proxy") - if err != nil { - c.logger.Printf("Could not resolve url to proxy at %s: %s", c, err) - c.scheduleReconnect() - return - } - switch u.Scheme { - case "http": - u.Scheme = "ws" - case "https": - u.Scheme = "wss" - } - - dialer := c.proxy.dialer - if c.ip != nil { - dialer = &websocket.Dialer{ - Proxy: http.ProxyFromEnvironment, - HandshakeTimeout: c.proxy.dialer.HandshakeTimeout, - TLSClientConfig: c.proxy.dialer.TLSClientConfig, - - // Override DNS lookup and connect to custom IP address. - NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - if _, port, err := net.SplitHostPort(addr); err == nil { - addr = net.JoinHostPort(c.ip.String(), port) - } - - return net.Dial(network, addr) - }, - } - } - conn, _, err := dialer.Dial(u.String(), nil) - if err != nil { - c.logger.Printf("Could not connect to %s: %s", c, err) - c.scheduleReconnect() - return - } - - c.logger.Printf("Connected to %s", c) - c.closed.Store(false) - c.connectedSince.Store(time.Now().UnixMicro()) - - c.mu.Lock() - c.helloProcessed = false - c.conn = conn - c.mu.Unlock() - - c.reconnectInterval.Store(int64(initialReconnectInterval)) - c.shutdownScheduled.Store(false) - if err := c.sendHello(); err != nil { - c.logger.Printf("Could not send hello request to %s: %s", c, err) - c.scheduleReconnect() - return - } - - if !c.sendPing() { - return - } - - go c.readPump() -} - -func (c *proxyConnection) waitUntilConnected(ctx context.Context) error { - c.mu.Lock() - defer c.mu.Unlock() - - if c.conn != nil { - return nil - } - - stop := context.AfterFunc(ctx, func() { - c.connectedCond.Broadcast() - }) - defer stop() - - for !c.helloProcessed { - if err := ctx.Err(); err != nil { - return err - } - - c.connectedCond.Wait() - } - - return nil -} - -func (c *proxyConnection) removePublisher(publisher *proxyPublisher) { - c.proxy.removePublisher(publisher) - - c.publishersLock.Lock() - defer c.publishersLock.Unlock() - - if _, found := c.publishers[publisher.proxyId]; found { - delete(c.publishers, publisher.proxyId) - sfuinternal.StatsPublishersCurrent.WithLabelValues(string(publisher.StreamType())).Dec() - } - delete(c.publisherIds, sfu.GetStreamId(publisher.id, publisher.StreamType())) - - if len(c.publishers) == 0 && (c.closeScheduled.Load() || c.IsTemporary()) { - go c.closeIfEmpty() - } -} - -func (c *proxyConnection) clearPublishers() { - c.publishersLock.Lock() - defer c.publishersLock.Unlock() - - go func(publishers map[string]*proxyPublisher) { - for _, publisher := range publishers { - publisher.NotifyClosed() - } - }(c.publishers) - // Can't use clear(...) here as the map is processed by the goroutine above. - c.publishers = make(map[string]*proxyPublisher) - clear(c.publisherIds) - - if c.closeScheduled.Load() || c.IsTemporary() { - go c.closeIfEmpty() - } -} - -func (c *proxyConnection) removeSubscriber(subscriber *proxySubscriber) { - c.subscribersLock.Lock() - defer c.subscribersLock.Unlock() - - if _, found := c.subscribers[subscriber.proxyId]; found { - delete(c.subscribers, subscriber.proxyId) - sfuinternal.StatsSubscribersCurrent.WithLabelValues(string(subscriber.StreamType())).Dec() - } - - if len(c.subscribers) == 0 && (c.closeScheduled.Load() || c.IsTemporary()) { - go c.closeIfEmpty() - } -} - -func (c *proxyConnection) clearSubscribers() { - c.subscribersLock.Lock() - defer c.subscribersLock.Unlock() - - go func(subscribers map[string]*proxySubscriber) { - for _, subscriber := range subscribers { - subscriber.NotifyClosed() - } - }(c.subscribers) - // Can't use clear(...) here as the map is processed by the goroutine above. - c.subscribers = make(map[string]*proxySubscriber) - - if c.closeScheduled.Load() || c.IsTemporary() { - go c.closeIfEmpty() - } -} - -func (c *proxyConnection) clearCallbacks() { - c.mu.Lock() - defer c.mu.Unlock() - - clear(c.callbacks) - clear(c.deferredCallbacks) -} - -func (c *proxyConnection) getCallback(id string) func(*proxy.ServerMessage) { - c.mu.Lock() - defer c.mu.Unlock() - - callback, found := c.callbacks[id] - if found { - delete(c.callbacks, id) - } - return callback -} - -func (c *proxyConnection) registerDeferredCallback(msgId string, callback mcuProxyCallback) { - if msgId == "" { - return - } - - c.mu.Lock() - defer c.mu.Unlock() - - delete(c.callbacks, msgId) - if c.deferredCallbacks == nil { - c.deferredCallbacks = make(map[string]mcuProxyCallback) - } - c.deferredCallbacks[msgId] = callback -} - -func (c *proxyConnection) getDeferredCallback(msgId string) mcuProxyCallback { - c.mu.Lock() - defer c.mu.Unlock() - - result, found := c.deferredCallbacks[msgId] - if found { - delete(c.deferredCallbacks, msgId) - } - - return result -} - -func (c *proxyConnection) processMessage(msg *proxy.ServerMessage) { - if c.helloMsgId != "" && msg.Id == c.helloMsgId { - c.helloMsgId = "" - switch msg.Type { - case "error": - if msg.Error.Code == "no_such_session" { - c.logger.Printf("Session %s could not be resumed on %s, registering new", c.SessionId(), c) - c.clearPublishers() - c.clearSubscribers() - c.clearCallbacks() - c.sessionId.Store(api.PublicSessionId("")) - if err := c.sendHello(); err != nil { - c.logger.Printf("Could not send hello request to %s: %s", c, err) - c.scheduleReconnect() - } - return - } - - c.logger.Printf("Hello connection to %s failed with %+v, reconnecting", c, msg.Error) - c.scheduleReconnect() - case "hello": - resumed := c.SessionId() == msg.Hello.SessionId - c.sessionId.Store(msg.Hello.SessionId) - var country geoip.Country - if server := msg.Hello.Server; server != nil { - if country = server.Country; country != "" && !geoip.IsValidCountry(country) { - c.logger.Printf("Proxy %s sent invalid country %s in hello response", c, country) - country = "" - } - c.version.Store(server.Version) - if server.Features == nil { - server.Features = []string{} - } - c.features.Store(server.Features) - } else { - c.version.Store("") - c.features.Store([]string{}) - } - c.country.Store(country) - if resumed { - c.logger.Printf("Resumed session %s on %s", c.SessionId(), c) - } else if country != "" { - c.logger.Printf("Received session %s from %s (in %s)", c.SessionId(), c, country) - } else { - c.logger.Printf("Received session %s from %s", c.SessionId(), c) - } - if c.trackClose.CompareAndSwap(false, true) { - statsConnectedProxyBackendsCurrent.WithLabelValues(string(c.Country())).Inc() - } - - c.mu.Lock() - c.helloProcessed = true - c.connectedCond.Broadcast() - c.mu.Unlock() - default: - c.logger.Printf("Received unsupported hello response %+v from %s, reconnecting", msg, c) - c.scheduleReconnect() - } - return - } - - if proxyDebugMessages { - c.logger.Printf("Received from %s: %+v", c, msg) - } - if callback := c.getCallback(msg.Id); callback != nil { - callback(msg) - return - } else if callback := c.getDeferredCallback(msg.Id); callback != nil { - go callback(msg) - return - } - - switch msg.Type { - case "payload": - c.processPayload(msg) - case "event": - c.processEvent(msg) - case "bye": - c.processBye(msg) - default: - c.logger.Printf("Unsupported message received from %s: %+v", c, msg) - } -} - -func (c *proxyConnection) processPayload(msg *proxy.ServerMessage) { - payload := msg.Payload - c.publishersLock.RLock() - publisher, found := c.publishers[payload.ClientId] - c.publishersLock.RUnlock() - if found { - publisher.ProcessPayload(payload) - return - } - - c.subscribersLock.RLock() - subscriber, found := c.subscribers[payload.ClientId] - c.subscribersLock.RUnlock() - if found { - subscriber.ProcessPayload(payload) - return - } - - c.logger.Printf("Received payload for unknown client %+v from %s", payload, c) -} - -func (c *proxyConnection) processEvent(msg *proxy.ServerMessage) { - event := msg.Event - switch event.Type { - case "backend-disconnected": - c.logger.Printf("Upstream backend at %s got disconnected, reset MCU objects", c) - c.clearPublishers() - c.clearSubscribers() - c.clearCallbacks() - // TODO: Should we also reconnect? - return - case "backend-connected": - c.logger.Printf("Upstream backend at %s is connected", c) - return - case "update-load": - if proxyDebugMessages { - c.logger.Printf("Load of %s now at %d (%s)", c, event.Load, event.Bandwidth) - } - c.load.Store(event.Load) - c.bandwidth.Store(event.Bandwidth) - statsProxyBackendLoadCurrent.WithLabelValues(c.url.String()).Set(float64(event.Load)) - if bw := event.Bandwidth; bw != nil { - statsProxyBandwidthCurrent.WithLabelValues(c.url.String(), "incoming").Set(float64(bw.Received.Bytes())) - statsProxyBandwidthCurrent.WithLabelValues(c.url.String(), "outgoing").Set(float64(bw.Sent.Bytes())) - if bw.Incoming != nil { - statsProxyUsageCurrent.WithLabelValues(c.url.String(), "incoming").Set(*bw.Incoming) - } else { - statsProxyUsageCurrent.WithLabelValues(c.url.String(), "incoming").Set(0) - } - if bw.Outgoing != nil { - statsProxyUsageCurrent.WithLabelValues(c.url.String(), "outgoing").Set(*bw.Outgoing) - } else { - statsProxyUsageCurrent.WithLabelValues(c.url.String(), "outgoing").Set(0) - } - } - return - case "shutdown-scheduled": - c.logger.Printf("Proxy %s is scheduled to shutdown", c) - c.shutdownScheduled.Store(true) - return - } - - if proxyDebugMessages { - c.logger.Printf("Process event from %s: %+v", c, event) - } - c.publishersLock.RLock() - publisher, found := c.publishers[event.ClientId] - c.publishersLock.RUnlock() - if found { - publisher.ProcessEvent(event) - return - } - - c.subscribersLock.RLock() - subscriber, found := c.subscribers[event.ClientId] - c.subscribersLock.RUnlock() - if found { - subscriber.ProcessEvent(event) - return - } - - c.logger.Printf("Received event for unknown client %+v from %s", event, c) -} - -func (c *proxyConnection) processBye(msg *proxy.ServerMessage) { - bye := msg.Bye - switch bye.Reason { - case "session_resumed": - c.logger.Printf("Session %s on %s was resumed by other client, resetting", c.SessionId(), c) - case "session_expired": - c.logger.Printf("Session %s expired on %s, resetting", c.SessionId(), c) - case "session_closed": - c.logger.Printf("Session %s was closed on %s, resetting", c.SessionId(), c) - default: - c.logger.Printf("Received bye with unsupported reason from %s %+v", c, bye) - } - c.sessionId.Store(api.PublicSessionId("")) -} - -func (c *proxyConnection) sendHello() error { - c.helloMsgId = strconv.FormatInt(c.msgId.Add(1), 10) - msg := &proxy.ClientMessage{ - Id: c.helloMsgId, - Type: "hello", - Hello: &proxy.HelloClientMessage{ - Version: "1.0", - }, - } - if sessionId := c.SessionId(); sessionId != "" { - msg.Hello.ResumeId = sessionId - } else if c.connectToken != "" { - msg.Hello.Token = c.connectToken - } else { - tokenString, err := c.proxy.CreateToken("") - if err != nil { - return err - } - - msg.Hello.Token = tokenString - } - return c.sendMessage(msg) -} - -func (c *proxyConnection) sendMessage(msg *proxy.ClientMessage) error { - c.mu.Lock() - defer c.mu.Unlock() - - return c.sendMessageLocked(msg) -} - -// +checklocks:c.mu -func (c *proxyConnection) sendMessageLocked(msg *proxy.ClientMessage) error { - if proxyDebugMessages { - c.logger.Printf("Send message to %s: %+v", c, msg) - } - if c.conn == nil { - return sfu.ErrNotConnected - } - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint - return c.conn.WriteJSON(msg) -} - -func (c *proxyConnection) performAsyncRequest(ctx context.Context, msg *proxy.ClientMessage, callback func(err error, response *proxy.ServerMessage)) string { - msgId := strconv.FormatInt(c.msgId.Add(1), 10) - msg.Id = msgId - - c.mu.Lock() - defer c.mu.Unlock() - c.callbacks[msgId] = func(msg *proxy.ServerMessage) { - callback(nil, msg) - } - if err := c.sendMessageLocked(msg); err != nil { - delete(c.callbacks, msgId) - go callback(err, nil) - return "" - } - - context.AfterFunc(ctx, func() { - c.mu.Lock() - defer c.mu.Unlock() - - delete(c.callbacks, msgId) - }) - - return msgId -} - -func (c *proxyConnection) performSyncRequest(ctx context.Context, msg *proxy.ClientMessage) (*proxy.ServerMessage, string, error) { - if err := ctx.Err(); err != nil { - return nil, "", err - } - - errChan := make(chan error, 1) - responseChan := make(chan *proxy.ServerMessage, 1) - msgId := c.performAsyncRequest(ctx, msg, func(err error, response *proxy.ServerMessage) { - if err != nil { - errChan <- err - } else { - responseChan <- response - } - }) - - select { - case <-ctx.Done(): - return nil, msgId, ctx.Err() - case err := <-errChan: - return nil, msgId, err - case response := <-responseChan: - return response, msgId, nil - } -} - -func (c *proxyConnection) deferredDeletePublisher(id api.PublicSessionId, streamType sfu.StreamType, response *proxy.ServerMessage) { - if response.Type == "error" { - c.logger.Printf("Publisher for %s was not created at %s: %s", id, c, response.Error) - return - } - - proxyId := response.Command.Id - c.logger.Printf("Created unused %s publisher %s on %s for %s", streamType, proxyId, c, id) - msg := &proxy.ClientMessage{ - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "delete-publisher", - ClientId: proxyId, - }, - } - - ctx, cancel := context.WithTimeout(context.Background(), c.proxy.settings.Timeout()) - defer cancel() - - if response, _, err := c.performSyncRequest(ctx, msg); err != nil { - c.logger.Printf("Could not delete publisher %s at %s: %s", proxyId, c, err) - return - } else if response.Type == "error" { - c.logger.Printf("Could not delete publisher %s at %s: %s", proxyId, c, response.Error) - return - } - - c.logger.Printf("Deleted publisher %s at %s", proxyId, c) -} - -func (c *proxyConnection) newPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings) (sfu.Publisher, error) { - msg := &proxy.ClientMessage{ - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "create-publisher", - Sid: sid, - StreamType: streamType, - PublisherSettings: &settings, - // Include for older version of the signaling proxy. - Bitrate: settings.Bitrate, - MediaTypes: settings.MediaTypes, - }, - } - - response, msgId, err := c.performSyncRequest(ctx, msg) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - c.registerDeferredCallback(msgId, func(response *proxy.ServerMessage) { - c.deferredDeletePublisher(id, streamType, response) - }) - } - return nil, err - } else if response.Type == "error" { - return nil, fmt.Errorf("error creating %s publisher for %s on %s: %+v", streamType, id, c, response.Error) - } - - proxyId := response.Command.Id - c.logger.Printf("Created %s publisher %s on %s for %s", streamType, proxyId, c, id) - publisher := newProxyPublisher(c.logger, id, sid, streamType, response.Command.Bitrate, settings, proxyId, c, listener) - c.publishersLock.Lock() - c.publishers[proxyId] = publisher - c.publisherIds[sfu.GetStreamId(id, streamType)] = api.PublicSessionId(proxyId) - c.publishersLock.Unlock() - sfuinternal.StatsPublishersCurrent.WithLabelValues(string(streamType)).Inc() - sfuinternal.StatsPublishersTotal.WithLabelValues(string(streamType)).Inc() - return publisher, nil -} - -func (c *proxyConnection) deferredDeleteSubscriber(publisherSessionId api.PublicSessionId, streamType sfu.StreamType, publisherConn *proxyConnection, response *proxy.ServerMessage) { - if response.Type == "error" { - c.logger.Printf("Subscriber for %s was not created at %s: %s", publisherSessionId, c, response.Error) - return - } - - proxyId := response.Command.Id - c.logger.Printf("Created unused %s subscriber %s on %s for %s", streamType, proxyId, c, publisherSessionId) - - msg := &proxy.ClientMessage{ - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "delete-subscriber", - ClientId: proxyId, - }, - } - - ctx, cancel := context.WithTimeout(context.Background(), c.proxy.settings.Timeout()) - defer cancel() - - if response, _, err := c.performSyncRequest(ctx, msg); err != nil { - if publisherConn != nil { - c.logger.Printf("Could not delete remote subscriber %s at %s (forwarded to %s): %s", proxyId, c, publisherConn, err) - } else { - c.logger.Printf("Could not delete subscriber %s at %s: %s", proxyId, c, err) - } - return - } else if response.Type == "error" { - if publisherConn != nil { - c.logger.Printf("Could not delete remote subscriber %s at %s (forwarded to %s): %s", proxyId, c, publisherConn, response.Error) - } else { - c.logger.Printf("Could not delete subscriber %s at %s: %s", proxyId, c, response.Error) - } - return - } - - if publisherConn != nil { - c.logger.Printf("Deleted remote subscriber %s at %s (forwarded to %s)", proxyId, c, publisherConn) - } else { - c.logger.Printf("Deleted subscriber %s at %s", proxyId, c) - } -} - -func (c *proxyConnection) newSubscriber(ctx context.Context, listener sfu.Listener, publisherId api.PublicSessionId, publisherSessionId api.PublicSessionId, streamType sfu.StreamType) (sfu.Subscriber, error) { - msg := &proxy.ClientMessage{ - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "create-subscriber", - StreamType: streamType, - PublisherId: publisherId, - }, - } - - response, msgId, err := c.performSyncRequest(ctx, msg) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - c.registerDeferredCallback(msgId, func(response *proxy.ServerMessage) { - c.deferredDeleteSubscriber(publisherSessionId, streamType, nil, response) - }) - } - return nil, err - } else if response.Type == "error" { - return nil, fmt.Errorf("error creating %s subscriber for %s on %s: %+v", streamType, publisherSessionId, c, response.Error) - } - - proxyId := response.Command.Id - c.logger.Printf("Created %s subscriber %s on %s for %s", streamType, proxyId, c, publisherSessionId) - subscriber := newProxySubscriber(c.logger, publisherSessionId, response.Command.Sid, streamType, response.Command.Bitrate, proxyId, c, listener, nil) - c.subscribersLock.Lock() - c.subscribers[proxyId] = subscriber - c.subscribersLock.Unlock() - sfuinternal.StatsSubscribersCurrent.WithLabelValues(string(streamType)).Inc() - sfuinternal.StatsSubscribersTotal.WithLabelValues(string(streamType)).Inc() - return subscriber, nil -} - -func (c *proxyConnection) newRemoteSubscriber(ctx context.Context, listener sfu.Listener, publisherId api.PublicSessionId, publisherSessionId api.PublicSessionId, streamType sfu.StreamType, publisherConn *proxyConnection, remoteToken string) (sfu.Subscriber, error) { - if c == publisherConn { - return c.newSubscriber(ctx, listener, publisherId, publisherSessionId, streamType) - } - - if remoteToken == "" { - var err error - if remoteToken, err = c.proxy.CreateToken(string(publisherId)); err != nil { - return nil, err - } - } - - msg := &proxy.ClientMessage{ - Type: "command", - Command: &proxy.CommandClientMessage{ - Type: "create-subscriber", - StreamType: streamType, - PublisherId: publisherId, - - RemoteUrl: publisherConn.rawUrl, - RemoteToken: remoteToken, - }, - } - - response, msgId, err := c.performSyncRequest(ctx, msg) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - c.registerDeferredCallback(msgId, func(response *proxy.ServerMessage) { - c.deferredDeleteSubscriber(publisherSessionId, streamType, publisherConn, response) - }) - } - return nil, err - } else if response.Type == "error" { - return nil, fmt.Errorf("error creating remote %s subscriber for %s on %s (forwarded to %s): %+v", streamType, publisherSessionId, c, publisherConn, response.Error) - } - - proxyId := response.Command.Id - c.logger.Printf("Created remote %s subscriber %s on %s for %s (forwarded to %s)", streamType, proxyId, c, publisherSessionId, publisherConn) - subscriber := newProxySubscriber(c.logger, publisherSessionId, response.Command.Sid, streamType, response.Command.Bitrate, proxyId, c, listener, publisherConn) - c.subscribersLock.Lock() - c.subscribers[proxyId] = subscriber - c.subscribersLock.Unlock() - sfuinternal.StatsSubscribersCurrent.WithLabelValues(string(streamType)).Inc() - sfuinternal.StatsSubscribersTotal.WithLabelValues(string(streamType)).Inc() - return subscriber, nil -} - -type proxySettings struct { - sfuinternal.CommonSettings -} - -func newProxySettings(ctx context.Context, config *goconf.ConfigFile) (sfu.Settings, error) { - settings := &proxySettings{ - CommonSettings: sfuinternal.CommonSettings{ - Logger: log.LoggerFromContext(ctx), - }, - } - if err := settings.load(config); err != nil { - return nil, err - } - - return settings, nil -} - -func (s *proxySettings) load(config *goconf.ConfigFile) error { - if err := s.Load(config); err != nil { - return err - } - - proxyTimeoutSeconds, _ := config.GetInt("mcu", "proxytimeout") - if proxyTimeoutSeconds <= 0 { - proxyTimeoutSeconds = defaultProxyTimeoutSeconds - } - proxyTimeout := time.Duration(proxyTimeoutSeconds) * time.Second - s.Logger.Printf("Using a timeout of %s for proxy requests", proxyTimeout) - s.SetTimeout(proxyTimeout) - return nil -} - -func (s *proxySettings) Reload(config *goconf.ConfigFile) { - if err := s.load(config); err != nil { - s.Logger.Printf("Error reloading proxy settings: %s", err) - } -} - -type proxySFU struct { - logger log.Logger - urlType string - tokenId string - tokenKey *rsa.PrivateKey - config Config - - dialer *websocket.Dialer - connectionsMu sync.RWMutex - // +checklocks:connectionsMu - connections []*proxyConnection - // +checklocks:connectionsMu - connectionsMap map[string][]*proxyConnection - connRequests atomic.Int64 - nextSort atomic.Int64 - - settings sfu.Settings - - mu sync.RWMutex - // +checklocks:mu - publishers map[sfu.StreamId]*proxyConnection - publishersCond sync.Cond - - continentsMap atomic.Value - - rpcClients *grpc.Clients -} - -func NewProxySFU(ctx context.Context, config *goconf.ConfigFile, etcdClient etcd.Client, rpcClients *grpc.Clients, dnsMonitor *dns.Monitor) (sfu.SFU, error) { - logger := log.LoggerFromContext(ctx) - urlType, _ := config.GetString("mcu", "urltype") - if urlType == "" { - urlType = proxyUrlTypeStatic - } - - tokenId, _ := config.GetString("mcu", "token_id") - if tokenId == "" { - return nil, errors.New("no token id configured") - } - tokenKeyFilename, _ := config.GetString("mcu", "token_key") - if tokenKeyFilename == "" { - return nil, errors.New("no token key configured") - } - tokenKeyData, err := os.ReadFile(tokenKeyFilename) - if err != nil { - return nil, fmt.Errorf("could not read private key from %s: %s", tokenKeyFilename, err) - } - tokenKey, err := jwt.ParseRSAPrivateKeyFromPEM(tokenKeyData) - if err != nil { - return nil, fmt.Errorf("could not parse private key from %s: %s", tokenKeyFilename, err) - } - - settings, err := newProxySettings(ctx, config) - if err != nil { - return nil, err - } - - mcu := &proxySFU{ - logger: logger, - urlType: urlType, - tokenId: tokenId, - tokenKey: tokenKey, - - dialer: &websocket.Dialer{ - Proxy: http.ProxyFromEnvironment, - HandshakeTimeout: settings.Timeout(), - }, - connectionsMap: make(map[string][]*proxyConnection), - settings: settings, - - publishers: make(map[sfu.StreamId]*proxyConnection), - - rpcClients: rpcClients, - } - mcu.publishersCond.L = &mcu.mu - - if err := mcu.loadContinentsMap(config); err != nil { - return nil, err - } - - skipverify, _ := config.GetBool("mcu", "skipverify") - if skipverify { - logger.Println("WARNING: MCU verification is disabled!") - mcu.dialer.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: skipverify, - } - } - - switch urlType { - case proxyUrlTypeStatic: - mcu.config, err = NewConfigStatic(logger, config, mcu, dnsMonitor) - case proxyUrlTypeEtcd: - mcu.config, err = NewConfigEtcd(logger, config, etcdClient, mcu) - default: - err = fmt.Errorf("unsupported proxy URL type %s", urlType) - } - if err != nil { - return nil, err - } - - return mcu, nil -} - -func (m *proxySFU) GetBandwidthLimits() (api.Bandwidth, api.Bandwidth) { - return m.settings.MaxStreamBitrate(), m.settings.MaxScreenBitrate() -} - -func (m *proxySFU) loadContinentsMap(cfg *goconf.ConfigFile) error { - options, err := config.GetStringOptions(cfg, "continent-overrides", false) - if err != nil { - return err - } - - if len(options) == 0 { - m.setContinentsMap(nil) - return nil - } - - continentsMap := make(ContinentsMap) - for option, value := range options { - option := geoip.Continent(strings.ToUpper(strings.TrimSpace(option))) - if !geoip.IsValidContinent(option) { - m.logger.Printf("Ignore unknown continent %s", option) - continue - } - - var values []geoip.Continent - for v := range internal.SplitEntries(value, ",") { - v := geoip.Continent(strings.ToUpper(v)) - if !geoip.IsValidContinent(v) { - m.logger.Printf("Ignore unknown continent %s for override %s", v, option) - continue - } - values = append(values, v) - } - if len(values) == 0 { - m.logger.Printf("No valid values found for continent override %s, ignoring", option) - continue - } - - continentsMap[option] = values - m.logger.Printf("Mapping users on continent %s to %s", option, values) - } - - m.setContinentsMap(continentsMap) - return nil -} - -func (m *proxySFU) Start(ctx context.Context) error { - return m.config.Start() -} - -func (m *proxySFU) Stop() { - m.connectionsMu.RLock() - defer m.connectionsMu.RUnlock() - - ctx, cancel := context.WithTimeout(context.Background(), closeTimeout) - defer cancel() - for _, c := range m.connections { - c.stop(ctx) - } - - m.config.Stop() -} - -func (m *proxySFU) CreateToken(subject string) (string, error) { - claims := &proxy.TokenClaims{ - RegisteredClaims: jwt.RegisteredClaims{ - IssuedAt: jwt.NewNumericDate(time.Now()), - Issuer: m.tokenId, - Subject: subject, - }, - } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - tokenString, err := token.SignedString(m.tokenKey) - if err != nil { - return "", err - } - - return tokenString, nil -} - -func (m *proxySFU) getConnections() []*proxyConnection { - m.connectionsMu.RLock() - defer m.connectionsMu.RUnlock() - - return m.connections -} - -func (m *proxySFU) ConnectionsCount() int { - m.connectionsMu.RLock() - defer m.connectionsMu.RUnlock() - - return len(m.connections) -} - -func (m *proxySFU) hasConnections() bool { - m.connectionsMu.RLock() - defer m.connectionsMu.RUnlock() - - return slices.ContainsFunc(m.connections, func(conn *proxyConnection) bool { - return conn.IsConnected() - }) -} - -func (m *proxySFU) WaitForDisconnected(ctx context.Context) error { - ticker := time.NewTicker(time.Millisecond) - defer ticker.Stop() - - for m.hasConnections() { - select { - case <-ctx.Done(): - return ctx.Err() - case <-ticker.C: - } - } - return nil -} - -func (m *proxySFU) WaitForConnections(ctx context.Context) error { - ticker := time.NewTicker(10 * time.Millisecond) - defer ticker.Stop() - - for !m.hasConnections() { - select { - case <-ctx.Done(): - return ctx.Err() - case <-ticker.C: - } - } - return nil -} - -func (m *proxySFU) WaitForConnectionsEstablished(ctx context.Context, waitMap map[string]bool) error { - m.connectionsMu.RLock() - defer m.connectionsMu.RUnlock() - - for len(waitMap) > 0 { - if err := ctx.Err(); err != nil { - return err - } - - for u := range waitMap { - for _, c := range m.connections { - if c.rawUrl == u && c.IsConnected() && c.SessionId() != "" { - delete(waitMap, u) - break - } - } - } - - m.connectionsMu.RUnlock() - time.Sleep(time.Millisecond) - m.connectionsMu.RLock() - } - - return nil -} - -func (m *proxySFU) AddConnection(ignoreErrors bool, url string, ips ...net.IP) error { - m.connectionsMu.Lock() - defer m.connectionsMu.Unlock() - - var conns []*proxyConnection - if len(ips) == 0 { - conn, err := newProxyConnection(m, url, nil, "") - if err != nil { - if ignoreErrors { - m.logger.Printf("Could not create proxy connection to %s: %s", url, err) - return nil - } - - return err - } - - conns = append(conns, conn) - } else { - for _, ip := range ips { - conn, err := newProxyConnection(m, url, ip, "") - if err != nil { - if ignoreErrors { - m.logger.Printf("Could not create proxy connection to %s (%s): %s", url, ip, err) - continue - } - - return err - } - - conns = append(conns, conn) - } - } - - for _, conn := range conns { - m.logger.Printf("Adding new connection to %s", conn) - conn.start() - - m.connections = append(m.connections, conn) - if existing, found := m.connectionsMap[url]; found { - m.connectionsMap[url] = append(existing, conn) - } else { - m.connectionsMap[url] = []*proxyConnection{conn} - } - } - - m.nextSort.Store(0) - return nil -} - -func (m *proxySFU) iterateConnections(url string, ips []net.IP) iter.Seq[*proxyConnection] { - return func(yield func(*proxyConnection) bool) { - m.connectionsMu.Lock() - defer m.connectionsMu.Unlock() - - conns, found := m.connectionsMap[url] - if !found { - return - } - - if len(ips) == 0 { - for _, conn := range conns { - if !yield(conn) { - return - } - } - } else { - for _, conn := range conns { - if slices.ContainsFunc(ips, func(i net.IP) bool { - return i.Equal(conn.ip) - }) { - if !yield(conn) { - return - } - } - } - } - } -} - -func (m *proxySFU) RemoveConnection(url string, ips ...net.IP) { - for conn := range m.iterateConnections(url, ips) { - m.logger.Printf("Removing connection to %s", conn) - conn.closeIfEmpty() - } -} - -func (m *proxySFU) KeepConnection(url string, ips ...net.IP) { - for conn := range m.iterateConnections(url, ips) { - conn.stopCloseIfEmpty() - conn.clearTemporary() - } -} - -func (m *proxySFU) Reload(config *goconf.ConfigFile) { - m.settings.Reload(config) - - if m.settings.Timeout() != m.dialer.HandshakeTimeout { - m.dialer.HandshakeTimeout = m.settings.Timeout() - } - - if err := m.loadContinentsMap(config); err != nil { - m.logger.Printf("Error loading continents map: %s", err) - } - - if err := m.config.Reload(config); err != nil { - m.logger.Printf("could not reload proxy configuration: %s", err) - } -} - -func (m *proxySFU) removeConnection(c *proxyConnection) { - m.connectionsMu.Lock() - defer m.connectionsMu.Unlock() - - if conns, found := m.connectionsMap[c.rawUrl]; found { - idx := slices.Index(conns, c) - if idx == -1 { - return - } - - conns = slices.Delete(conns, idx, idx+1) - if len(conns) == 0 { - delete(m.connectionsMap, c.rawUrl) - } else { - m.connectionsMap[c.rawUrl] = conns - } - - m.connections = nil - for _, conns := range m.connectionsMap { - m.connections = append(m.connections, conns...) - } - - m.nextSort.Store(0) - } -} - -func (m *proxySFU) SetOnConnected(f func()) { - // Not supported. -} - -func (m *proxySFU) SetOnDisconnected(f func()) { - // Not supported. -} - -type mcuProxyStats struct { - Publishers int64 `json:"publishers"` - Clients int64 `json:"clients"` - Details []*proxyConnectionStats `json:"details"` -} - -func (m *proxySFU) GetStats() any { - result := &mcuProxyStats{} - - m.connectionsMu.RLock() - defer m.connectionsMu.RUnlock() - - for _, conn := range m.connections { - stats := conn.GetStats() - result.Publishers += stats.Publishers - result.Clients += stats.Clients - result.Details = append(result.Details, stats) - } - return result -} - -func (m *proxySFU) GetServerInfoSfu() *talk.BackendServerInfoSfu { - m.connectionsMu.RLock() - defer m.connectionsMu.RUnlock() - - sfu := &talk.BackendServerInfoSfu{ - Mode: talk.SfuModeProxy, - } - for _, c := range m.connections { - proxy := talk.BackendServerInfoSfuProxy{ - Url: c.rawUrl, - - Temporary: c.IsTemporary(), - } - if len(c.ip) > 0 { - proxy.IP = c.ip.String() - } - if c.IsConnected() { - proxy.Connected = true - proxy.Shutdown = internal.MakePtr(c.IsShutdownScheduled()) - if since := c.connectedSince.Load(); since != 0 { - t := time.UnixMicro(since) - proxy.Uptime = &t - } - proxy.Version = c.Version() - proxy.Features = c.Features() - proxy.Country = c.Country() - proxy.Load = internal.MakePtr(c.Load()) - if bw := c.Bandwidth(); bw != nil { - proxy.Bandwidth = &talk.BackendServerInfoSfuProxyBandwidth{ - Incoming: bw.Incoming, - Outgoing: bw.Outgoing, - Received: bw.Received, - Sent: bw.Sent, - } - } - } - sfu.Proxies = append(sfu.Proxies, proxy) - } - slices.SortFunc(sfu.Proxies, func(a, b talk.BackendServerInfoSfuProxy) int { - c := strings.Compare(a.Url, b.Url) - if c == 0 { - c = strings.Compare(a.IP, b.IP) - } - return c - }) - return sfu -} - -func (m *proxySFU) getContinentsMap() ContinentsMap { - continentsMap := m.continentsMap.Load() - if continentsMap == nil { - return nil - } - return continentsMap.(ContinentsMap) -} - -func (m *proxySFU) setContinentsMap(continentsMap ContinentsMap) { - if continentsMap == nil { - continentsMap = make(ContinentsMap) - } - m.continentsMap.Store(continentsMap) -} - -type mcuProxyConnectionsList []*proxyConnection - -func ContinentsOverlap(a, b []geoip.Continent) bool { - if len(a) == 0 || len(b) == 0 { - return false - } - - for _, checkA := range a { - if slices.Contains(b, checkA) { - return true - } - } - return false -} - -func sortConnectionsForCountry(connections []*proxyConnection, country geoip.Country, continentMap ContinentsMap) []*proxyConnection { - // Move connections in the same country to the start of the list. - sorted := make(mcuProxyConnectionsList, 0, len(connections)) - unprocessed := make(mcuProxyConnectionsList, 0, len(connections)) - for _, conn := range connections { - if country == conn.Country() { - sorted = append(sorted, conn) - } else { - unprocessed = append(unprocessed, conn) - } - } - if continents, found := geoip.ContinentMap[country]; found && len(unprocessed) > 1 { - remaining := make(mcuProxyConnectionsList, 0, len(unprocessed)) - // Map continents to other continents (e.g. use Europe for Africa). - for _, continent := range continents { - if toAdd, found := continentMap[continent]; found { - continents = append(continents, toAdd...) - } - } - - // Next up are connections on the same or mapped continent. - for _, conn := range unprocessed { - connCountry := conn.Country() - if geoip.IsValidCountry(connCountry) { - connContinents := geoip.ContinentMap[connCountry] - if ContinentsOverlap(continents, connContinents) { - sorted = append(sorted, conn) - } else { - remaining = append(remaining, conn) - } - } else { - remaining = append(remaining, conn) - } - } - unprocessed = remaining - } - // Add all other connections by load. - sorted = append(sorted, unprocessed...) - return sorted -} - -func (m *proxySFU) getSortedConnections(initiator sfu.Initiator) []*proxyConnection { - m.connectionsMu.RLock() - connections := m.connections - m.connectionsMu.RUnlock() - if len(connections) < 2 { - return connections - } - - // Connections are re-sorted every requests or - // every . - now := time.Now().UnixNano() - if m.connRequests.Add(1)%connectionSortRequests == 0 || m.nextSort.Load() <= now { - m.nextSort.Store(now + int64(connectionSortInterval)) - - sorted := slices.Clone(connections) - slices.SortFunc(sorted, func(a, b *proxyConnection) int { - return int(a.Load() - b.Load()) - }) - - m.connectionsMu.Lock() - m.connections = sorted - m.connectionsMu.Unlock() - connections = sorted - } - - if initiator != nil { - if country := initiator.Country(); geoip.IsValidCountry(country) { - connections = sortConnectionsForCountry(connections, country, m.getContinentsMap()) - } - } - return connections -} - -func (m *proxySFU) removePublisher(publisher *proxyPublisher) { - m.mu.Lock() - defer m.mu.Unlock() - - delete(m.publishers, sfu.GetStreamId(publisher.id, publisher.StreamType())) -} - -func (m *proxySFU) createPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator, connections []*proxyConnection, isAllowed func(c *proxyConnection) bool) sfu.Publisher { - var maxBitrate api.Bandwidth - if streamType == sfu.StreamTypeScreen { - maxBitrate = m.settings.MaxScreenBitrate() - } else { - maxBitrate = m.settings.MaxStreamBitrate() - } - - publisherSettings := settings - if publisherSettings.Bitrate <= 0 { - publisherSettings.Bitrate = maxBitrate - } else { - publisherSettings.Bitrate = min(publisherSettings.Bitrate, maxBitrate) - } - - for _, conn := range connections { - if !isAllowed(conn) || conn.IsShutdownScheduled() || conn.IsTemporary() { - continue - } - - subctx, cancel := context.WithTimeout(ctx, m.settings.Timeout()) - defer cancel() - - publisher, err := conn.newPublisher(subctx, listener, id, sid, streamType, publisherSettings) - if err != nil { - m.logger.Printf("Could not create %s publisher for %s on %s: %s", streamType, id, conn, err) - continue - } - - m.mu.Lock() - defer m.mu.Unlock() - m.publishers[sfu.GetStreamId(id, streamType)] = conn - m.publishersCond.Broadcast() - return publisher - } - - return nil -} - -func (m *proxySFU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { - connections := m.getSortedConnections(initiator) - publisher := m.createPublisher(ctx, listener, id, sid, streamType, settings, initiator, connections, func(c *proxyConnection) bool { - bw := c.Bandwidth() - return bw == nil || bw.AllowIncoming() - }) - if publisher == nil { - // No proxy has available bandwidth, select one with the lowest currently used bandwidth. - connections2 := make([]*proxyConnection, 0, len(connections)) - for _, c := range connections { - if c.Bandwidth() != nil { - connections2 = append(connections2, c) - } - } - slices.SortFunc(connections2, func(a *proxyConnection, b *proxyConnection) int { - var incoming_a *float64 - if bw := a.Bandwidth(); bw != nil { - incoming_a = bw.Incoming - } - - var incoming_b *float64 - if bw := b.Bandwidth(); bw != nil { - incoming_b = bw.Incoming - } - - switch { - case incoming_a == nil && incoming_b == nil: - return 0 - case incoming_a == nil && incoming_b != nil: - return -1 - case incoming_a != nil && incoming_b == nil: - return -1 - case *incoming_a < *incoming_b: - return -1 - case *incoming_a > *incoming_b: - return 1 - default: - return 0 - } - }) - publisher = m.createPublisher(ctx, listener, id, sid, streamType, settings, initiator, connections2, func(c *proxyConnection) bool { - return true - }) - } - - if publisher == nil { - statsProxyNobackendAvailableTotal.WithLabelValues(string(streamType)).Inc() - return nil, errors.New("no MCU connection available") - } - - return publisher, nil -} - -func (m *proxySFU) getPublisherConnection(publisher api.PublicSessionId, streamType sfu.StreamType) *proxyConnection { - m.mu.RLock() - defer m.mu.RUnlock() - - return m.publishers[sfu.GetStreamId(publisher, streamType)] -} - -func (m *proxySFU) waitForPublisherConnection(ctx context.Context, publisher api.PublicSessionId, streamType sfu.StreamType) *proxyConnection { - m.mu.Lock() - defer m.mu.Unlock() - - conn := m.publishers[sfu.GetStreamId(publisher, streamType)] - if conn != nil { - // Publisher was created while waiting for lock. - return conn - } - - stop := context.AfterFunc(ctx, func() { - m.publishersCond.Broadcast() - }) - defer stop() - - sfuinternal.StatsWaitingForPublisherTotal.WithLabelValues(string(streamType)).Inc() - for conn == nil { - if err := ctx.Err(); err != nil { - return nil - } - - m.publishersCond.Wait() - conn = m.publishers[sfu.GetStreamId(publisher, streamType)] - } - return conn -} - -type proxyPublisherInfo struct { - id api.PublicSessionId - conn *proxyConnection - token string - err error -} - -func (m *proxySFU) createSubscriber(ctx context.Context, listener sfu.Listener, info *proxyPublisherInfo, publisher api.PublicSessionId, streamType sfu.StreamType, connections []*proxyConnection, isAllowed func(c *proxyConnection) bool) sfu.Subscriber { - for _, conn := range connections { - if !isAllowed(conn) || conn.IsShutdownScheduled() || conn.IsTemporary() { - continue - } - - subctx, cancel := context.WithTimeout(ctx, m.settings.Timeout()) - defer cancel() - - var subscriber sfu.Subscriber - var err error - if conn == info.conn { - subscriber, err = conn.newSubscriber(subctx, listener, info.id, publisher, streamType) - } else { - subscriber, err = conn.newRemoteSubscriber(subctx, listener, info.id, publisher, streamType, info.conn, info.token) - } - if err != nil { - m.logger.Printf("Could not create subscriber for %s publisher %s on %s: %s", streamType, publisher, conn, err) - continue - } - - return subscriber - } - - return nil -} - -func (m *proxySFU) NewSubscriber(ctx context.Context, listener sfu.Listener, publisher api.PublicSessionId, streamType sfu.StreamType, initiator sfu.Initiator) (sfu.Subscriber, error) { - var publisherInfo *proxyPublisherInfo - if conn := m.getPublisherConnection(publisher, streamType); conn != nil { - // Fast common path: publisher is available locally. - conn.publishersLock.Lock() - id, found := conn.publisherIds[sfu.GetStreamId(publisher, streamType)] - conn.publishersLock.Unlock() - if !found { - return nil, fmt.Errorf("unknown publisher %s", publisher) - } - - publisherInfo = &proxyPublisherInfo{ - id: id, - conn: conn, - } - } else { - m.logger.Printf("No %s publisher %s found yet, deferring", streamType, publisher) - ch := make(chan *proxyPublisherInfo, 1) - getctx, cancel := context.WithCancel(ctx) - defer cancel() - - var wg sync.WaitGroup - - // Wait for publisher to be created locally. - wg.Go(func() { - if conn := m.waitForPublisherConnection(getctx, publisher, streamType); conn != nil { - cancel() // Cancel pending RPC calls. - - conn.publishersLock.Lock() - id, found := conn.publisherIds[sfu.GetStreamId(publisher, streamType)] - conn.publishersLock.Unlock() - if !found { - ch <- &proxyPublisherInfo{ - err: fmt.Errorf("unknown id for local %s publisher %s", streamType, publisher), - } - return - } - - ch <- &proxyPublisherInfo{ - id: id, - conn: conn, - } - } - }) - - // Wait for publisher to be created on one of the other servers in the cluster. - if clients := m.rpcClients.GetClients(); len(clients) > 0 { - for _, client := range clients { - wg.Go(func() { - id, url, ip, connectToken, publisherToken, err := client.GetPublisherId(getctx, publisher, streamType) - if errors.Is(err, context.Canceled) { - return - } else if err != nil { - m.logger.Printf("Error getting %s publisher id %s from %s: %s", streamType, publisher, client.Target(), err) - return - } else if id == "" { - // Publisher not found on other server - return - } - - cancel() // Cancel pending RPC calls. - m.logger.Printf("Found publisher id %s through %s on proxy %s", id, client.Target(), url) - - m.connectionsMu.RLock() - connections := m.connections - m.connectionsMu.RUnlock() - var publisherConn *proxyConnection - for _, conn := range connections { - if conn.rawUrl != url || !ip.Equal(conn.ip) { - continue - } - - // Simple case, signaling server has a connection to the same endpoint - publisherConn = conn - break - } - - if publisherConn == nil { - publisherConn, err = newProxyConnection(m, url, ip, connectToken) - if err != nil { - m.logger.Printf("Could not create temporary connection to %s for %s publisher %s: %s", url, streamType, publisher, err) - return - } - - publisherConn.setTemporary() - publisherConn.start() - if err := publisherConn.waitUntilConnected(ctx); err != nil { - m.logger.Printf("Could not establish new connection to %s: %s", publisherConn, err) - publisherConn.closeIfEmpty() - return - } - - m.connectionsMu.Lock() - m.connections = append(m.connections, publisherConn) - conns, found := m.connectionsMap[url] - if found { - conns = append(conns, publisherConn) - } else { - conns = []*proxyConnection{publisherConn} - } - m.connectionsMap[url] = conns - m.connectionsMu.Unlock() - } - - ch <- &proxyPublisherInfo{ - id: id, - conn: publisherConn, - token: publisherToken, - } - }) - } - } - - wg.Wait() - select { - case ch <- &proxyPublisherInfo{ - err: fmt.Errorf("no %s publisher %s found", streamType, publisher), - }: - default: - } - - select { - case info := <-ch: - publisherInfo = info - case <-ctx.Done(): - return nil, fmt.Errorf("no %s publisher %s found", streamType, publisher) - } - } - - if publisherInfo.err != nil { - return nil, publisherInfo.err - } - - bw := publisherInfo.conn.Bandwidth() - allowOutgoing := bw == nil || bw.AllowOutgoing() - if !allowOutgoing || !publisherInfo.conn.IsSameCountry(initiator) { - connections := m.getSortedConnections(initiator) - if !allowOutgoing || len(connections) > 0 && !connections[0].IsSameCountry(publisherInfo.conn) { - // Connect to remote publisher through "closer" gateway. - subscriber := m.createSubscriber(ctx, listener, publisherInfo, publisher, streamType, connections, func(c *proxyConnection) bool { - bw := c.Bandwidth() - return bw == nil || bw.AllowOutgoing() - }) - if subscriber == nil { - connections2 := make([]*proxyConnection, 0, len(connections)) - for _, c := range connections { - if c.Bandwidth() != nil { - connections2 = append(connections2, c) - } - } - slices.SortFunc(connections2, func(a *proxyConnection, b *proxyConnection) int { - var outgoing_a *float64 - if bw := a.Bandwidth(); bw != nil { - outgoing_a = bw.Outgoing - } - - var outgoing_b *float64 - if bw := b.Bandwidth(); bw != nil { - outgoing_b = bw.Outgoing - } - - switch { - case outgoing_a == nil && outgoing_b == nil: - return 0 - case outgoing_a == nil && outgoing_b != nil: - return -1 - case outgoing_a != nil && outgoing_b == nil: - return -1 - case *outgoing_a < *outgoing_b: - return -1 - case *outgoing_a > *outgoing_b: - return 1 - default: - return 0 - } - }) - subscriber = m.createSubscriber(ctx, listener, publisherInfo, publisher, streamType, connections2, func(c *proxyConnection) bool { - return true - }) - } - if subscriber != nil { - return subscriber, nil - } - } - } - - subctx, cancel := context.WithTimeout(ctx, m.settings.Timeout()) - defer cancel() - - subscriber, err := publisherInfo.conn.newSubscriber(subctx, listener, publisherInfo.id, publisher, streamType) - if err != nil { - if publisherInfo.conn.IsTemporary() { - publisherInfo.conn.closeIfEmpty() - } - m.logger.Printf("Could not create subscriber for %s publisher %s on %s: %s", streamType, publisher, publisherInfo.conn, err) - return nil, err - } - - return subscriber, nil -} diff --git a/sfu/proxy/proxy_test.go b/sfu/proxy/proxy_test.go deleted file mode 100644 index 9bab3e9..0000000 --- a/sfu/proxy/proxy_test.go +++ /dev/null @@ -1,1747 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2020 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package proxy - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "encoding/json" - "fmt" - "net" - "net/url" - "path" - "slices" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - dnstest "github.com/strukturag/nextcloud-spreed-signaling/v2/dns/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc" - grpctest "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - metricstest "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/mock" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/proxy/testserver" -) - -const ( - testTimeout = 10 * time.Second -) - -func TestMcuProxyStats(t *testing.T) { - t.Parallel() - metricstest.CollectAndLint(t, proxyMcuStats...) -} - -func newProxyConnectionWithCountry(country geoip.Country) *proxyConnection { - conn := &proxyConnection{} - conn.country.Store(country) - return conn -} - -func Test_sortConnectionsForCountry(t *testing.T) { - t.Parallel() - conn_de := newProxyConnectionWithCountry("DE") - conn_at := newProxyConnectionWithCountry("AT") - conn_jp := newProxyConnectionWithCountry("JP") - conn_us := newProxyConnectionWithCountry("US") - - testcases := map[geoip.Country][][]*proxyConnection{ - // Direct country match - "DE": { - {conn_at, conn_jp, conn_de}, - {conn_de, conn_at, conn_jp}, - }, - // Direct country match - "AT": { - {conn_at, conn_jp, conn_de}, - {conn_at, conn_de, conn_jp}, - }, - // Continent match - "CH": { - {conn_de, conn_jp, conn_at}, - {conn_de, conn_at, conn_jp}, - }, - // Direct country match - "JP": { - {conn_de, conn_jp, conn_at}, - {conn_jp, conn_de, conn_at}, - }, - // Continent match - "CN": { - {conn_de, conn_jp, conn_at}, - {conn_jp, conn_de, conn_at}, - }, - // Continent match - "RU": { - {conn_us, conn_de, conn_jp, conn_at}, - {conn_de, conn_at, conn_us, conn_jp}, - }, - // No match - "AU": { - {conn_us, conn_de, conn_jp, conn_at}, - {conn_us, conn_de, conn_jp, conn_at}, - }, - } - - for country, test := range testcases { - t.Run(string(country), func(t *testing.T) { - t.Parallel() - sorted := sortConnectionsForCountry(test[0], country, nil) - for idx, conn := range sorted { - assert.Equal(t, test[1][idx], conn, "Index %d for %s: expected %s, got %s", idx, country, test[1][idx].Country(), conn.Country()) - } - }) - } -} - -func Test_sortConnectionsForCountryWithOverride(t *testing.T) { - t.Parallel() - conn_de := newProxyConnectionWithCountry("DE") - conn_at := newProxyConnectionWithCountry("AT") - conn_jp := newProxyConnectionWithCountry("JP") - conn_us := newProxyConnectionWithCountry("US") - - testcases := map[geoip.Country][][]*proxyConnection{ - // Direct country match - "DE": { - {conn_at, conn_jp, conn_de}, - {conn_de, conn_at, conn_jp}, - }, - // Direct country match - "AT": { - {conn_at, conn_jp, conn_de}, - {conn_at, conn_de, conn_jp}, - }, - // Continent match - "CH": { - {conn_de, conn_jp, conn_at}, - {conn_de, conn_at, conn_jp}, - }, - // Direct country match - "JP": { - {conn_de, conn_jp, conn_at}, - {conn_jp, conn_de, conn_at}, - }, - // Continent match - "CN": { - {conn_de, conn_jp, conn_at}, - {conn_jp, conn_de, conn_at}, - }, - // Continent match - "RU": { - {conn_us, conn_de, conn_jp, conn_at}, - {conn_de, conn_at, conn_us, conn_jp}, - }, - // No match - "AR": { - {conn_us, conn_de, conn_jp, conn_at}, - {conn_us, conn_de, conn_jp, conn_at}, - }, - // No match but override (OC -> AS / NA) - "AU": { - {conn_us, conn_jp}, - {conn_us, conn_jp}, - }, - // No match but override (AF -> EU) - "ZA": { - {conn_de, conn_at}, - {conn_de, conn_at}, - }, - } - - continentMap := ContinentsMap{ - // Use European connections for Africa. - "AF": {"EU"}, - // Use Asian and North American connections for Oceania. - "OC": {"AS", "NA"}, - } - for country, test := range testcases { - t.Run(string(country), func(t *testing.T) { - t.Parallel() - sorted := sortConnectionsForCountry(test[0], country, continentMap) - for idx, conn := range sorted { - assert.Equal(t, test[1][idx], conn, "Index %d for %s: expected %s, got %s", idx, country, test[1][idx].Country(), conn.Country()) - } - }) - } -} - -func newMcuProxyForTestWithOptions(t *testing.T, options testserver.ProxyTestOptions, idx int, lookup *dnstest.MockLookup) (*proxySFU, *goconf.ConfigFile) { - t.Helper() - require := require.New(t) - if options.Etcd == nil { - options.Etcd = etcdtest.NewServerForTest(t) - } - grpcClients, dnsMonitor := grpctest.NewClientsWithEtcdForTest(t, options.Etcd, lookup) - - tokenKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - dir := t.TempDir() - privkeyFile := path.Join(dir, "privkey.pem") - pubkeyFile := path.Join(dir, "pubkey.pem") - require.NoError(internal.WritePrivateKey(tokenKey, privkeyFile)) - require.NoError(internal.WritePublicKey(&tokenKey.PublicKey, pubkeyFile)) - - cfg := goconf.NewConfigFile() - cfg.AddOption("mcu", "urltype", "static") - if strings.Contains(t.Name(), "DnsDiscovery") { - cfg.AddOption("mcu", "dnsdiscovery", "true") - } - cfg.AddOption("mcu", "proxytimeout", strconv.Itoa(int(testTimeout.Seconds()))) - var urls []string - waitingMap := make(map[string]bool) - if len(options.Servers) == 0 { - options.Servers = []testserver.ProxyTestServer{ - testserver.NewProxyServerForTest(t, "DE"), - } - } - tokenId := fmt.Sprintf("test-token-%d", idx) - for _, s := range options.Servers { - s.SetServers(options.Servers) - s.SetToken(tokenId, &tokenKey.PublicKey) - urls = append(urls, s.URL()) - waitingMap[s.URL()] = true - } - cfg.AddOption("mcu", "url", strings.Join(urls, " ")) - cfg.AddOption("mcu", "token_id", tokenId) - cfg.AddOption("mcu", "token_key", privkeyFile) - - etcdConfig := goconf.NewConfigFile() - etcdConfig.AddOption("etcd", "endpoints", options.Etcd.URL().String()) - etcdConfig.AddOption("etcd", "loglevel", "error") - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - etcdClient, err := etcd.NewClient(logger, etcdConfig, "") - require.NoError(err) - t.Cleanup(func() { - assert.NoError(t, etcdClient.Close()) - }) - - mcu, err := NewProxySFU(ctx, cfg, etcdClient, grpcClients, dnsMonitor) - require.NoError(err) - t.Cleanup(func() { - mcu.Stop() - }) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(mcu.Start(ctx)) - - proxy := mcu.(*proxySFU) - - require.NoError(proxy.WaitForConnections(ctx)) - - for len(waitingMap) > 0 { - require.NoError(ctx.Err()) - - for u := range waitingMap { - proxy.connectionsMu.RLock() - connections := proxy.connections - proxy.connectionsMu.RUnlock() - for _, c := range connections { - if c.rawUrl == u && c.IsConnected() && c.SessionId() != "" { - delete(waitingMap, u) - break - } - } - } - - time.Sleep(time.Millisecond) - } - - return proxy, cfg -} - -func newMcuProxyForTestWithServers(t *testing.T, servers []testserver.ProxyTestServer, idx int, lookup *dnstest.MockLookup) *proxySFU { - t.Helper() - - proxy, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: servers, - }, idx, lookup) - return proxy -} - -func newMcuProxyForTest(t *testing.T, idx int, lookup *dnstest.MockLookup) *proxySFU { - t.Helper() - server := testserver.NewProxyServerForTest(t, "DE") - - return newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{server}, idx, lookup) -} - -func Test_ProxyAddRemoveConnections(t *testing.T) { - t.Parallel() - assert := assert.New(t) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - server1 := testserver.NewProxyServerForTest(t, "DE") - mcu, config := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: []testserver.ProxyTestServer{ - server1, - }, - }, 0, nil) - - server2 := testserver.NewProxyServerForTest(t, "DE") - server1.Servers = append(server1.Servers, server2) - server2.Servers = server1.Servers - server2.Tokens = server1.Tokens - urls1 := []string{ - server1.URL(), - server2.URL(), - } - config.AddOption("mcu", "url", strings.Join(urls1, " ")) - mcu.Reload(config) - - mcu.connectionsMu.RLock() - assert.Len(mcu.connections, 2) - mcu.connectionsMu.RUnlock() - - // Wait until connection is established. - waitCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - - for waitCtx.Err() == nil { - mcu.connectionsMu.RLock() - notAllConnected := slices.ContainsFunc(mcu.connections, func(conn *proxyConnection) bool { - return !conn.IsConnected() - }) - mcu.connectionsMu.RUnlock() - if notAllConnected { - time.Sleep(time.Millisecond) - continue - } - - break - } - assert.NoError(waitCtx.Err(), "error while waiting for connection to be established") - - urls2 := []string{ - server2.URL(), - } - config.AddOption("mcu", "url", strings.Join(urls2, " ")) - mcu.Reload(config) - - // Removing the connections takes a short while (asynchronously, closed when unused). - waitCtx, cancel = context.WithTimeout(ctx, time.Second) - defer cancel() - - for waitCtx.Err() == nil { - mcu.connectionsMu.RLock() - if len(mcu.connections) != 1 { - mcu.connectionsMu.RUnlock() - time.Sleep(time.Millisecond) - continue - } - - assert.Len(mcu.connections, 1) - assert.Equal(server2.URL(), mcu.connections[0].rawUrl) - mcu.connectionsMu.RUnlock() - break - } - assert.NoError(waitCtx.Err(), "error while waiting for connection to be removed") -} - -func Test_ProxyAddRemoveConnectionsDnsDiscovery(t *testing.T) { - t.Parallel() - assert := assert.New(t) - require := require.New(t) - - lookup := dnstest.NewMockLookup() - - server1 := testserver.NewProxyServerForTest(t, "DE") - server1.Start() - h, port, err := net.SplitHostPort(server1.Listener().Addr().String()) - require.NoError(err) - ip1 := net.ParseIP(h) - require.NotNil(ip1, "failed for %s", h) - - require.Contains(server1.URL(), ip1.String()) - server1.SetURL(strings.ReplaceAll(server1.URL(), ip1.String(), "proxydomain.invalid")) - u1, err := url.Parse(server1.URL()) - require.NoError(err) - lookup.Set(u1.Hostname(), []net.IP{ - ip1, - }) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - mcu, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: []testserver.ProxyTestServer{ - server1, - }, - }, 0, lookup) - - if connections := mcu.getConnections(); assert.Len(connections, 1) && assert.NotNil(connections[0].ip) { - assert.True(ip1.Equal(connections[0].ip), "ip addresses differ: expected %s, got %s", ip1.String(), connections[0].ip.String()) - } - - dnsMonitor := mcu.config.(*configStatic).dnsMonitor - require.NotNil(dnsMonitor) - - server2 := testserver.NewProxyServerForTest(t, "DE") - l, err := net.Listen("tcp", "127.0.0.2:"+port) - require.NoError(err) - assert.NoError(server2.Listener().Close()) - server2.SetListener(l) - server2.Start() - - h, _, err = net.SplitHostPort(server2.Listener().Addr().String()) - require.NoError(err) - ip2 := net.ParseIP(h) - require.NotNil(ip2, "failed for %s", h) - require.Contains(server2.URL(), ip2.String()) - server2.SetURL(strings.ReplaceAll(server2.URL(), ip2.String(), "proxydomain.invalid")) - - server1.Servers = append(server1.Servers, server2) - server2.Servers = server1.Servers - server2.Tokens = server1.Tokens - - lookup.Set(u1.Hostname(), []net.IP{ - ip1, - ip2, - }) - dnsMonitor.CheckHostnames() - - // Wait until connection is established. - waitCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - - for waitCtx.Err() == nil { - mcu.connectionsMu.RLock() - if len(mcu.connections) != 2 { - mcu.connectionsMu.RUnlock() - time.Sleep(time.Millisecond) - continue - } - - notAllConnected := slices.ContainsFunc(mcu.connections, func(conn *proxyConnection) bool { - return !conn.IsConnected() - }) - mcu.connectionsMu.RUnlock() - if notAllConnected { - time.Sleep(time.Millisecond) - continue - } - - break - } - assert.NoError(waitCtx.Err(), "error while waiting for connection to be established") - - lookup.Set(u1.Hostname(), []net.IP{ - ip2, - }) - dnsMonitor.CheckHostnames() - - // Removing the connections takes a short while (asynchronously, closed when unused). - waitCtx, cancel = context.WithTimeout(ctx, time.Second) - defer cancel() - - for waitCtx.Err() == nil { - mcu.connectionsMu.RLock() - if len(mcu.connections) != 1 { - mcu.connectionsMu.RUnlock() - time.Sleep(time.Millisecond) - continue - } - - assert.Len(mcu.connections, 1) - assert.Equal(server1.URL(), mcu.connections[0].rawUrl) - if assert.NotNil(mcu.connections[0].ip) { - assert.True(ip2.Equal(mcu.connections[0].ip), "ip addresses differ: expected %s, got %s", ip2.String(), mcu.connections[0].ip.String()) - } - mcu.connectionsMu.RUnlock() - break - } - assert.NoError(waitCtx.Err(), "error while waiting for connection to be removed") -} - -func Test_ProxyPublisherSubscriber(t *testing.T) { - t.Parallel() - mcu := newMcuProxyForTest(t, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("DE") - - sub, err := mcu.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(t, err) - - defer sub.Close(context.Background()) -} - -func Test_ProxyPublisherCodecs(t *testing.T) { - t.Parallel() - mcu := newMcuProxyForTest(t, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - AudioCodec: "opus,g722", - VideoCodec: "vp9,vp8,av1", - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) -} - -func Test_ProxyWaitForPublisher(t *testing.T) { - t.Parallel() - mcu := newMcuProxyForTest(t, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("DE") - done := make(chan struct{}) - go func() { - defer close(done) - sub, err := mcu.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - if !assert.NoError(t, err) { - return - } - - defer sub.Close(context.Background()) - }() - - // Give subscriber goroutine some time to start - time.Sleep(100 * time.Millisecond) - - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - select { - case <-done: - case <-ctx.Done(): - assert.NoError(t, ctx.Err()) - } - defer pub.Close(context.Background()) -} - -func Test_ProxyPublisherBandwidth(t *testing.T) { - t.Parallel() - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "DE") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - server1, - server2, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pub1Id := api.PublicSessionId("the-publisher-1") - pub1Sid := "1234567890" - pub1Listener := mock.NewListener(pub1Id + "-public") - pub1Initiator := mock.NewInitiator("DE") - pub1, err := mcu.NewPublisher(ctx, pub1Listener, pub1Id, pub1Sid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pub1Initiator) - require.NoError(t, err) - - defer pub1.Close(context.Background()) - - if pub1.(*proxyPublisher).conn.rawUrl == server1.URL() { - server1.UpdateBandwidth(100, 0) - } else { - server2.UpdateBandwidth(100, 0) - } - - // Wait until proxy has been updated - for assert.NoError(t, ctx.Err()) { - mcu.connectionsMu.RLock() - connections := mcu.connections - mcu.connectionsMu.RUnlock() - missing := true - for _, c := range connections { - if c.Bandwidth() != nil { - missing = false - break - } - } - if !missing { - break - } - time.Sleep(time.Millisecond) - } - - pub2Id := api.PublicSessionId("the-publisher-2") - pub2id := "1234567890" - pub2Listener := mock.NewListener(pub2Id + "-public") - pub2Initiator := mock.NewInitiator("DE") - pub2, err := mcu.NewPublisher(ctx, pub2Listener, pub2Id, pub2id, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pub2Initiator) - require.NoError(t, err) - - defer pub2.Close(context.Background()) - - assert.NotEqual(t, pub1.(*proxyPublisher).conn.rawUrl, pub2.(*proxyPublisher).conn.rawUrl) -} - -func Test_ProxyPublisherBandwidthOverload(t *testing.T) { - t.Parallel() - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "DE") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - server1, - server2, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pub1Id := api.PublicSessionId("the-publisher-1") - pub1Sid := "1234567890" - pub1Listener := mock.NewListener(pub1Id + "-public") - pub1Initiator := mock.NewInitiator("DE") - pub1, err := mcu.NewPublisher(ctx, pub1Listener, pub1Id, pub1Sid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pub1Initiator) - require.NoError(t, err) - - defer pub1.Close(context.Background()) - - // If all servers are bandwidth loaded, select the one with the least usage. - if pub1.(*proxyPublisher).conn.rawUrl == server1.URL() { - server1.UpdateBandwidth(100, 0) - server2.UpdateBandwidth(102, 0) - } else { - server1.UpdateBandwidth(102, 0) - server2.UpdateBandwidth(100, 0) - } - - // Wait until proxy has been updated - for assert.NoError(t, ctx.Err()) { - mcu.connectionsMu.RLock() - connections := mcu.connections - mcu.connectionsMu.RUnlock() - missing := false - for _, c := range connections { - if c.Bandwidth() == nil { - missing = true - break - } - } - if !missing { - break - } - time.Sleep(time.Millisecond) - } - - pub2Id := api.PublicSessionId("the-publisher-2") - pub2id := "1234567890" - pub2Listener := mock.NewListener(pub2Id + "-public") - pub2Initiator := mock.NewInitiator("DE") - pub2, err := mcu.NewPublisher(ctx, pub2Listener, pub2Id, pub2id, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pub2Initiator) - require.NoError(t, err) - - defer pub2.Close(context.Background()) - - assert.Equal(t, pub1.(*proxyPublisher).conn.rawUrl, pub2.(*proxyPublisher).conn.rawUrl) -} - -func Test_ProxyPublisherLoad(t *testing.T) { - t.Parallel() - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "DE") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - server1, - server2, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pub1Id := api.PublicSessionId("the-publisher-1") - pub1Sid := "1234567890" - pub1Listener := mock.NewListener(pub1Id + "-public") - pub1Initiator := mock.NewInitiator("DE") - pub1, err := mcu.NewPublisher(ctx, pub1Listener, pub1Id, pub1Sid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pub1Initiator) - require.NoError(t, err) - - defer pub1.Close(context.Background()) - - // Make sure connections are re-sorted. - mcu.nextSort.Store(0) - time.Sleep(100 * time.Millisecond) - - pub2Id := api.PublicSessionId("the-publisher-2") - pub2id := "1234567890" - pub2Listener := mock.NewListener(pub2Id + "-public") - pub2Initiator := mock.NewInitiator("DE") - pub2, err := mcu.NewPublisher(ctx, pub2Listener, pub2Id, pub2id, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pub2Initiator) - require.NoError(t, err) - - defer pub2.Close(context.Background()) - - assert.NotEqual(t, pub1.(*proxyPublisher).conn.rawUrl, pub2.(*proxyPublisher).conn.rawUrl) -} - -func Test_ProxyPublisherCountry(t *testing.T) { - t.Parallel() - serverDE := testserver.NewProxyServerForTest(t, "DE") - serverUS := testserver.NewProxyServerForTest(t, "US") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - serverDE, - serverUS, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubDEId := api.PublicSessionId("the-publisher-de") - pubDESid := "1234567890" - pubDEListener := mock.NewListener(pubDEId + "-public") - pubDEInitiator := mock.NewInitiator("DE") - pubDE, err := mcu.NewPublisher(ctx, pubDEListener, pubDEId, pubDESid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubDEInitiator) - require.NoError(t, err) - - defer pubDE.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), pubDE.(*proxyPublisher).conn.rawUrl) - - pubUSId := api.PublicSessionId("the-publisher-us") - pubUSSid := "1234567890" - pubUSListener := mock.NewListener(pubUSId + "-public") - pubUSInitiator := mock.NewInitiator("US") - pubUS, err := mcu.NewPublisher(ctx, pubUSListener, pubUSId, pubUSSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubUSInitiator) - require.NoError(t, err) - - defer pubUS.Close(context.Background()) - - assert.Equal(t, serverUS.URL(), pubUS.(*proxyPublisher).conn.rawUrl) -} - -func Test_ProxyPublisherContinent(t *testing.T) { - t.Parallel() - serverDE := testserver.NewProxyServerForTest(t, "DE") - serverUS := testserver.NewProxyServerForTest(t, "US") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - serverDE, - serverUS, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubDEId := api.PublicSessionId("the-publisher-de") - pubDESid := "1234567890" - pubDEListener := mock.NewListener(pubDEId + "-public") - pubDEInitiator := mock.NewInitiator("DE") - pubDE, err := mcu.NewPublisher(ctx, pubDEListener, pubDEId, pubDESid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubDEInitiator) - require.NoError(t, err) - - defer pubDE.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), pubDE.(*proxyPublisher).conn.rawUrl) - - pubFRId := api.PublicSessionId("the-publisher-fr") - pubFRSid := "1234567890" - pubFRListener := mock.NewListener(pubFRId + "-public") - pubFRInitiator := mock.NewInitiator("FR") - pubFR, err := mcu.NewPublisher(ctx, pubFRListener, pubFRId, pubFRSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubFRInitiator) - require.NoError(t, err) - - defer pubFR.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), pubFR.(*proxyPublisher).conn.rawUrl) -} - -func Test_ProxySubscriberCountry(t *testing.T) { - t.Parallel() - serverDE := testserver.NewProxyServerForTest(t, "DE") - serverUS := testserver.NewProxyServerForTest(t, "US") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - serverDE, - serverUS, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), pub.(*proxyPublisher).conn.rawUrl) - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("US") - sub, err := mcu.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(t, err) - - defer sub.Close(context.Background()) - - assert.Equal(t, serverUS.URL(), sub.(*proxySubscriber).conn.rawUrl) -} - -func Test_ProxySubscriberContinent(t *testing.T) { - t.Parallel() - serverDE := testserver.NewProxyServerForTest(t, "DE") - serverUS := testserver.NewProxyServerForTest(t, "US") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - serverDE, - serverUS, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), pub.(*proxyPublisher).conn.rawUrl) - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("FR") - sub, err := mcu.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(t, err) - - defer sub.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), sub.(*proxySubscriber).conn.rawUrl) -} - -func Test_ProxySubscriberBandwidth(t *testing.T) { - t.Parallel() - serverDE := testserver.NewProxyServerForTest(t, "DE") - serverUS := testserver.NewProxyServerForTest(t, "US") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - serverDE, - serverUS, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), pub.(*proxyPublisher).conn.rawUrl) - - serverDE.UpdateBandwidth(0, 100) - - // Wait until proxy has been updated - for assert.NoError(t, ctx.Err()) { - mcu.connectionsMu.RLock() - connections := mcu.connections - mcu.connectionsMu.RUnlock() - missing := true - for _, c := range connections { - if c.Bandwidth() != nil { - missing = false - break - } - } - if !missing { - break - } - time.Sleep(time.Millisecond) - } - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("US") - sub, err := mcu.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(t, err) - - defer sub.Close(context.Background()) - - assert.Equal(t, serverUS.URL(), sub.(*proxySubscriber).conn.rawUrl) -} - -func Test_ProxySubscriberBandwidthOverload(t *testing.T) { - t.Parallel() - serverDE := testserver.NewProxyServerForTest(t, "DE") - serverUS := testserver.NewProxyServerForTest(t, "US") - mcu := newMcuProxyForTestWithServers(t, []testserver.ProxyTestServer{ - serverDE, - serverUS, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), pub.(*proxyPublisher).conn.rawUrl) - - serverDE.UpdateBandwidth(0, 100) - serverUS.UpdateBandwidth(0, 102) - - // Wait until proxy has been updated - for assert.NoError(t, ctx.Err()) { - mcu.connectionsMu.RLock() - connections := mcu.connections - mcu.connectionsMu.RUnlock() - missing := false - for _, c := range connections { - if c.Bandwidth() == nil { - missing = true - break - } - } - if !missing { - break - } - time.Sleep(time.Millisecond) - } - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("US") - sub, err := mcu.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(t, err) - - defer sub.Close(context.Background()) - - assert.Equal(t, serverDE.URL(), sub.(*proxySubscriber).conn.rawUrl) -} - -func Test_ProxyPublisherTimeout(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - server := testserver.NewProxyServerForTest(t, "DE") - mcu, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: []testserver.ProxyTestServer{server}, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - settings := mcu.settings.(*proxySettings) - settings.SetTimeout(testserver.TimeoutTestTimeout) - - // Creating the publisher will timeout locally. - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - if pub != nil { - defer pub.Close(context.Background()) - } - assert.ErrorContains(err, "no MCU connection available") - - // Wait for publisher to be created on the proxy side. - require.NoError(server.WaitForWakeup(ctx), "publisher not created") - - // The local side will remove the (unused) publisher from the proxy. - require.NoError(server.WaitForWakeup(ctx), "unused publisher not deleted") -} - -func Test_ProxySubscriberTimeout(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - server := testserver.NewProxyServerForTest(t, "DE") - mcu, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: []testserver.ProxyTestServer{server}, - }, 0, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - pub, err := mcu.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(err) - defer pub.Close(context.Background()) - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("DE") - - settings := mcu.settings.(*proxySettings) - settings.SetTimeout(testserver.TimeoutTestTimeout) - - // Creating the subscriber will timeout locally. - sub, err := mcu.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - if sub != nil { - defer sub.Close(context.Background()) - } - assert.ErrorIs(err, context.DeadlineExceeded) - - // Wait for subscriber to be created on the proxy side. - require.NoError(server.WaitForWakeup(ctx), "subscriber not created") - - // The local side will remove the (unused) subscriber from the proxy. - require.NoError(server.WaitForWakeup(ctx), "unused subscriber not deleted") -} - -func Test_ProxyReconnectAfter(t *testing.T) { - t.Parallel() - reasons := []string{ - "session_resumed", - "session_expired", - "session_closed", - "unknown_reason", - } - for _, reason := range reasons { - t.Run(reason, func(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - server := testserver.NewProxyServerForTest(t, "DE") - mcu, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: []testserver.ProxyTestServer{server}, - }, 0, nil) - - connections := mcu.getSortedConnections(nil) - require.Len(connections, 1) - sessionId := connections[0].SessionId() - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - client := server.GetSingleClient() - require.NotNil(client) - - client.SendMessage(&proxy.ServerMessage{ - Type: "bye", - Bye: &proxy.ByeServerMessage{ - Reason: reason, - }, - }) - - // The "bye" will close the connection and reset the session id. - assert.NoError(mcu.WaitForDisconnected(ctx)) - - // The client will automatically reconnect. - time.Sleep(10 * time.Millisecond) - assert.NoError(mcu.WaitForConnections(ctx)) - - if connections := mcu.getSortedConnections(nil); assert.Len(connections, 1) { - assert.NotEqual(sessionId, connections[0].SessionId()) - } - }) - } -} - -func Test_ProxyReconnectAfterShutdown(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - server := testserver.NewProxyServerForTest(t, "DE") - mcu, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: []testserver.ProxyTestServer{server}, - }, 0, nil) - - connections := mcu.getSortedConnections(nil) - require.Len(connections, 1) - sessionId := connections[0].SessionId() - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - client := server.GetSingleClient() - require.NotNil(client) - - client.SendMessage(&proxy.ServerMessage{ - Type: "event", - Event: &proxy.EventServerMessage{ - Type: "shutdown-scheduled", - }, - }) - - // Force reconnect. - client.Close() - assert.NoError(mcu.WaitForDisconnected(ctx)) - - // The client will automatically reconnect and resume the session. - time.Sleep(10 * time.Millisecond) - assert.NoError(mcu.WaitForConnections(ctx)) - - if connections := mcu.getSortedConnections(nil); assert.Len(connections, 1) { - assert.Equal(sessionId, connections[0].SessionId()) - } -} - -func Test_ProxyResume(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - server := testserver.NewProxyServerForTest(t, "DE") - mcu, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: []testserver.ProxyTestServer{server}, - }, 0, nil) - - connections := mcu.getSortedConnections(nil) - require.Len(connections, 1) - sessionId := connections[0].SessionId() - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - client := server.GetSingleClient() - require.NotNil(client) - - // Force reconnect. - client.Close() - assert.NoError(mcu.WaitForDisconnected(ctx)) - - // The client will automatically reconnect. - time.Sleep(10 * time.Millisecond) - assert.NoError(mcu.WaitForConnections(ctx)) - - if connections := mcu.getSortedConnections(nil); assert.Len(connections, 1) { - assert.Equal(sessionId, connections[0].SessionId()) - } -} - -func Test_ProxyResumeFail(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - server := testserver.NewProxyServerForTest(t, "DE") - mcu, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Servers: []testserver.ProxyTestServer{server}, - }, 0, nil) - - connections := mcu.getSortedConnections(nil) - require.Len(connections, 1) - sessionId := connections[0].SessionId() - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - client := server.GetSingleClient() - require.NotNil(client) - server.ClearClients() - - // Force reconnect. - client.Close() - assert.NoError(mcu.WaitForDisconnected(ctx)) - - // The client will automatically reconnect. - time.Sleep(10 * time.Millisecond) - assert.NoError(mcu.WaitForConnections(ctx)) - - if connections := mcu.getSortedConnections(nil); assert.Len(connections, 1) { - assert.NotEqual(sessionId, connections[0].SessionId()) - } -} - -type publisherHub struct { - grpctest.MockHub - - mu sync.Mutex - // +checklocks:mu - publishers map[api.PublicSessionId]*proxyPublisher - publishersCond sync.Cond -} - -func newPublisherHub() *publisherHub { - hub := &publisherHub{ - publishers: make(map[api.PublicSessionId]*proxyPublisher), - } - hub.publishersCond.L = &hub.mu - return hub -} - -func (h *publisherHub) addPublisher(publisher *proxyPublisher) { - h.mu.Lock() - defer h.mu.Unlock() - - h.publishers[publisher.PublisherId()] = publisher - h.publishersCond.Broadcast() -} - -func (h *publisherHub) GetPublisherIdForSessionId(ctx context.Context, sessionId api.PublicSessionId, streamType sfu.StreamType) (*grpc.GetPublisherIdReply, error) { - h.mu.Lock() - defer h.mu.Unlock() - - stop := context.AfterFunc(ctx, func() { - h.publishersCond.Broadcast() - }) - defer stop() - - pub, found := h.publishers[sessionId] - for !found { - if err := ctx.Err(); err != nil { - return nil, err - } - - h.publishersCond.Wait() - pub, found = h.publishers[sessionId] - } - - connToken, err := pub.conn.proxy.CreateToken("") - if err != nil { - return nil, err - } - pubToken, err := pub.conn.proxy.CreateToken(string(pub.Id())) - if err != nil { - return nil, err - } - - reply := &grpc.GetPublisherIdReply{ - PublisherId: pub.Id(), - ProxyUrl: pub.conn.rawUrl, - ConnectToken: connToken, - PublisherToken: pubToken, - } - if ip := pub.conn.ip; len(ip) > 0 { - reply.Ip = ip.String() - } - return reply, nil -} - -func Test_ProxyRemotePublisher(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - embedEtcd := etcdtest.NewServerForTest(t) - - grpcServer1, addr1 := grpctest.NewServerForTest(t) - grpcServer2, addr2 := grpctest.NewServerForTest(t) - - hub1 := newPublisherHub() - hub2 := newPublisherHub() - grpcServer1.SetHub(hub1) - grpcServer2.SetHub(hub2) - - embedEtcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) - - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "DE") - - mcu1, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - server2, - }, - }, 1, nil) - mcu2, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - server2, - }, - }, 2, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - if proxyPub, ok := pub.(*proxyPublisher); assert.True(ok) { - hub1.addPublisher(proxyPub) - } - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("DE") - sub, err := mcu2.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(t, err) - - defer sub.Close(context.Background()) -} - -func Test_ProxyMultipleRemotePublisher(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - embedEtcd := etcdtest.NewServerForTest(t) - - grpcServer1, addr1 := grpctest.NewServerForTest(t) - grpcServer2, addr2 := grpctest.NewServerForTest(t) - grpcServer3, addr3 := grpctest.NewServerForTest(t) - - hub1 := newPublisherHub() - hub2 := newPublisherHub() - hub3 := newPublisherHub() - grpcServer1.SetHub(hub1) - grpcServer2.SetHub(hub2) - grpcServer3.SetHub(hub3) - - embedEtcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) - embedEtcd.SetValue("/grpctargets/three", []byte("{\"address\":\""+addr3+"\"}")) - - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "US") - server3 := testserver.NewProxyServerForTest(t, "US") - - mcu1, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - server2, - server3, - }, - }, 1, nil) - mcu2, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - server2, - server3, - }, - }, 2, nil) - mcu3, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - server2, - server3, - }, - }, 3, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - if proxyPub, ok := pub.(*proxyPublisher); assert.True(ok) { - hub1.addPublisher(proxyPub) - } - - sub1Listener := mock.NewListener("subscriber-public-1") - sub1Initiator := mock.NewInitiator("US") - sub1, err := mcu2.NewSubscriber(ctx, sub1Listener, pubId, sfu.StreamTypeVideo, sub1Initiator) - require.NoError(t, err) - - defer sub1.Close(context.Background()) - - sub2Listener := mock.NewListener("subscriber-public-2") - sub2Initiator := mock.NewInitiator("US") - sub2, err := mcu3.NewSubscriber(ctx, sub2Listener, pubId, sfu.StreamTypeVideo, sub2Initiator) - require.NoError(t, err) - - defer sub2.Close(context.Background()) -} - -func Test_ProxyRemotePublisherWait(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - embedEtcd := etcdtest.NewServerForTest(t) - - grpcServer1, addr1 := grpctest.NewServerForTest(t) - grpcServer2, addr2 := grpctest.NewServerForTest(t) - - hub1 := newPublisherHub() - hub2 := newPublisherHub() - grpcServer1.SetHub(hub1) - grpcServer2.SetHub(hub2) - - embedEtcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) - - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "DE") - - mcu1, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - server2, - }, - }, 1, nil) - mcu2, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - server2, - }, - }, 2, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("DE") - - done := make(chan struct{}) - go func() { - defer close(done) - sub, err := mcu2.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - if !assert.NoError(err) { - return - } - - defer sub.Close(context.Background()) - }() - - // Give subscriber goroutine some time to start - time.Sleep(100 * time.Millisecond) - - pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - if proxyPub, ok := pub.(*proxyPublisher); assert.True(ok) { - hub1.addPublisher(proxyPub) - } - - select { - case <-done: - case <-ctx.Done(): - assert.NoError(ctx.Err()) - } -} - -func Test_ProxyRemotePublisherTemporary(t *testing.T) { - t.Parallel() - - require := require.New(t) - assert := assert.New(t) - embedEtcd := etcdtest.NewServerForTest(t) - - server, addr1 := grpctest.NewServerForTest(t) - hub := newPublisherHub() - server.SetHub(hub) - - target := grpc.TargetInformationEtcd{ - Address: addr1, - } - encoded, err := json.Marshal(target) - require.NoError(err) - embedEtcd.SetValue("/grpctargets/server", encoded) - - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "DE") - - mcu1, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - }, - }, 1, nil) - mcu2, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server2, - }, - }, 2, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(err) - - defer pub.Close(context.Background()) - - if proxyPub, ok := pub.(*proxyPublisher); assert.True(ok) { - hub.addPublisher(proxyPub) - } - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("DE") - sub, err := mcu2.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(err) - - defer sub.Close(context.Background()) - - if connSub, ok := sub.(*proxySubscriber); assert.True(ok) { - assert.Equal(server1.URL(), connSub.conn.rawUrl) - assert.Empty(connSub.conn.ip) - } - - // The temporary connection has been added - assert.Equal(2, mcu2.ConnectionsCount()) - - sub.Close(context.Background()) - - // Wait for temporary connection to be removed. -loop: - for { - select { - case <-ctx.Done(): - assert.NoError(ctx.Err()) - default: - if mcu2.ConnectionsCount() == 1 { - break loop - } - } - } -} - -func Test_ProxyConnectToken(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - embedEtcd := etcdtest.NewServerForTest(t) - - grpcServer1, addr1 := grpctest.NewServerForTest(t) - grpcServer2, addr2 := grpctest.NewServerForTest(t) - - hub1 := newPublisherHub() - hub2 := newPublisherHub() - grpcServer1.SetHub(hub1) - grpcServer2.SetHub(hub2) - - embedEtcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) - - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "DE") - - // Signaling server instances are in a cluster but don't share their proxies, - // i.e. they are only known to their local proxy, not the one of the other - // signaling server - so the connection token must be passed between them. - mcu1, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - }, - }, 1, nil) - mcu2, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server2, - }, - }, 2, nil) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - if proxyPub, ok := pub.(*proxyPublisher); assert.True(ok) { - hub1.addPublisher(proxyPub) - } - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("DE") - sub, err := mcu2.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(t, err) - - defer sub.Close(context.Background()) -} - -func Test_ProxyPublisherToken(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - embedEtcd := etcdtest.NewServerForTest(t) - - grpcServer1, addr1 := grpctest.NewServerForTest(t) - grpcServer2, addr2 := grpctest.NewServerForTest(t) - - hub1 := newPublisherHub() - hub2 := newPublisherHub() - grpcServer1.SetHub(hub1) - grpcServer2.SetHub(hub2) - - embedEtcd.SetValue("/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) - embedEtcd.SetValue("/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) - - server1 := testserver.NewProxyServerForTest(t, "DE") - server2 := testserver.NewProxyServerForTest(t, "US") - - // Signaling server instances are in a cluster but don't share their proxies, - // i.e. they are only known to their local proxy, not the one of the other - // signaling server - so the connection token must be passed between them. - // Also the subscriber is connecting from a different country, so a remote - // stream will be created that needs a valid token from the remote proxy. - mcu1, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server1, - }, - }, 1, nil) - mcu2, _ := newMcuProxyForTestWithOptions(t, testserver.ProxyTestOptions{ - Etcd: embedEtcd, - Servers: []testserver.ProxyTestServer{ - server2, - }, - }, 2, nil) - // Support remote subscribers for the tests. - server1.Servers = append(server1.Servers, server2) - server2.Servers = append(server2.Servers, server1) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - pubId := api.PublicSessionId("the-publisher") - pubSid := "1234567890" - pubListener := mock.NewListener(pubId + "-public") - pubInitiator := mock.NewInitiator("DE") - - pub, err := mcu1.NewPublisher(ctx, pubListener, pubId, pubSid, sfu.StreamTypeVideo, sfu.NewPublisherSettings{ - MediaTypes: sfu.MediaTypeVideo | sfu.MediaTypeAudio, - }, pubInitiator) - require.NoError(t, err) - - defer pub.Close(context.Background()) - - if proxyPub, ok := pub.(*proxyPublisher); assert.True(ok) { - hub1.addPublisher(proxyPub) - } - - subListener := mock.NewListener("subscriber-public") - subInitiator := mock.NewInitiator("US") - sub, err := mcu2.NewSubscriber(ctx, subListener, pubId, sfu.StreamTypeVideo, subInitiator) - require.NoError(t, err) - - defer sub.Close(context.Background()) -} diff --git a/sfu/proxy/stats_prometheus.go b/sfu/proxy/stats_prometheus.go deleted file mode 100644 index 1984f1c..0000000 --- a/sfu/proxy/stats_prometheus.go +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2021 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package proxy - -import ( - "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/internal" -) - -var ( - statsConnectedProxyBackendsCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "backend_connections", - Help: "Current number of connections to signaling proxy backends", - }, []string{"country"}) - statsProxyBackendLoadCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "backend_load", - Help: "Current load of signaling proxy backends", - }, []string{"url"}) - statsProxyUsageCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "backend_usage", - Help: "The current usage of signaling proxy backends in percent", - }, []string{"url", "direction"}) - statsProxyBandwidthCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "backend_bandwidth", - Help: "The current bandwidth of signaling proxy backends in bytes per second", - }, []string{"url", "direction"}) - statsProxyNobackendAvailableTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "mcu", - Name: "no_backend_available_total", - Help: "Total number of publishing requests where no backend was available", - }, []string{"type"}) - - proxyMcuStats = []prometheus.Collector{ - statsConnectedProxyBackendsCurrent, - statsProxyBackendLoadCurrent, - statsProxyUsageCurrent, - statsProxyBandwidthCurrent, - statsProxyNobackendAvailableTotal, - } -) - -func RegisterStats() { - internal.RegisterCommonStats() - metrics.RegisterAll(proxyMcuStats...) -} - -func UnregisterStats() { - internal.UnregisterCommonStats() - metrics.UnregisterAll(proxyMcuStats...) -} diff --git a/sfu/proxy/test/proxy.go b/sfu/proxy/test/proxy.go deleted file mode 100644 index c3d44ec..0000000 --- a/sfu/proxy/test/proxy.go +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "fmt" - "path" - "strconv" - "strings" - "testing" - "time" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - dnstest "github.com/strukturag/nextcloud-spreed-signaling/v2/dns/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" - grpctest "github.com/strukturag/nextcloud-spreed-signaling/v2/grpc/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/proxy" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu/proxy/testserver" -) - -const ( - testTimeout = 10 * time.Second -) - -type ConnectionWaiter interface { - WaitForConnections(ctx context.Context) error - WaitForConnectionsEstablished(ctx context.Context, waitMap map[string]bool) error -} - -func NewMcuProxyForTestWithOptions(t *testing.T, options testserver.ProxyTestOptions, idx int, lookup *dnstest.MockLookup) (sfu.SFU, *goconf.ConfigFile) { - t.Helper() - require := require.New(t) - require.NotEmpty(options.Servers) - if options.Etcd == nil { - options.Etcd = etcdtest.NewServerForTest(t) - } - grpcClients, dnsMonitor := grpctest.NewClientsWithEtcdForTest(t, options.Etcd, lookup) - - tokenKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(err) - dir := t.TempDir() - privkeyFile := path.Join(dir, "privkey.pem") - pubkeyFile := path.Join(dir, "pubkey.pem") - require.NoError(internal.WritePrivateKey(tokenKey, privkeyFile)) - require.NoError(internal.WritePublicKey(&tokenKey.PublicKey, pubkeyFile)) - - cfg := goconf.NewConfigFile() - cfg.AddOption("mcu", "urltype", "static") - if strings.Contains(t.Name(), "DnsDiscovery") { - cfg.AddOption("mcu", "dnsdiscovery", "true") - } - cfg.AddOption("mcu", "proxytimeout", strconv.Itoa(int(testTimeout.Seconds()))) - var urls []string - waitingMap := make(map[string]bool) - tokenId := fmt.Sprintf("test-token-%d", idx) - for _, s := range options.Servers { - s.SetServers(options.Servers) - s.SetToken(tokenId, &tokenKey.PublicKey) - urls = append(urls, s.URL()) - waitingMap[s.URL()] = true - } - cfg.AddOption("mcu", "url", strings.Join(urls, " ")) - cfg.AddOption("mcu", "token_id", tokenId) - cfg.AddOption("mcu", "token_key", privkeyFile) - - etcdConfig := goconf.NewConfigFile() - etcdConfig.AddOption("etcd", "endpoints", options.Etcd.URL().String()) - etcdConfig.AddOption("etcd", "loglevel", "error") - - logger := logtest.NewLoggerForTest(t) - ctx := log.NewLoggerContext(t.Context(), logger) - etcdClient, err := etcd.NewClient(logger, etcdConfig, "") - require.NoError(err) - t.Cleanup(func() { - assert.NoError(t, etcdClient.Close()) - }) - - mcu, err := proxy.NewProxySFU(ctx, cfg, etcdClient, grpcClients, dnsMonitor) - require.NoError(err) - t.Cleanup(func() { - mcu.Stop() - }) - - ctx, cancel := context.WithTimeout(t.Context(), testTimeout) - defer cancel() - - require.NoError(mcu.Start(ctx)) - - waiter, ok := mcu.(ConnectionWaiter) - require.True(ok, "can't wait for connections") - - require.NoError(waiter.WaitForConnections(ctx)) - require.NoError(waiter.WaitForConnectionsEstablished(ctx, waitingMap)) - - return mcu, cfg -} diff --git a/sfu/proxy/testserver/server.go b/sfu/proxy/testserver/server.go deleted file mode 100644 index c0c25e4..0000000 --- a/sfu/proxy/testserver/server.go +++ /dev/null @@ -1,753 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2026 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package testserver - -import ( - "context" - "crypto/rsa" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/http/httptest" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/golang-jwt/jwt/v5" - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - etcdtest "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/proxy" -) - -const ( - TimeoutTestTimeout = 100 * time.Millisecond -) - -type ProxyTestServer interface { - URL() string - SetServers(servers []ProxyTestServer) - SetToken(tokenId string, key *rsa.PublicKey) - - getToken(tokenId string) (*rsa.PublicKey, bool) - getPublisher(id api.PublicSessionId) *testProxyServerPublisher -} - -type ProxyTestOptions struct { - Etcd *etcdtest.EtcdServer - Servers []ProxyTestServer -} - -type proxyServerClientHandler func(msg *proxy.ClientMessage) (*proxy.ServerMessage, error) - -type testProxyServerPublisher struct { - id api.PublicSessionId -} - -type testProxyServerSubscriber struct { - id string - sid string - pub *testProxyServerPublisher - - remoteUrl string -} - -type TestProxyClient interface { - Close() - SendMessage(msg *proxy.ServerMessage) -} - -type testProxyServerClient struct { - t *testing.T - - server *TestProxyServerHandler - // +checklocks:mu - ws *websocket.Conn - processMessage proxyServerClientHandler - - mu sync.Mutex - sessionId api.PublicSessionId -} - -func (c *testProxyServerClient) processHello(msg *proxy.ClientMessage) (*proxy.ServerMessage, error) { - if msg.Type != "hello" { - return nil, fmt.Errorf("expected hello, got %+v", msg) - } - - if msg.Hello.ResumeId != "" { - client := c.server.getClient(msg.Hello.ResumeId) - if client == nil { - response := &proxy.ServerMessage{ - Id: msg.Id, - Type: "error", - Error: &api.Error{ - Code: "no_such_session", - }, - } - return response, nil - } - - c.sessionId = msg.Hello.ResumeId - c.server.setClient(c.sessionId, c) - response := &proxy.ServerMessage{ - Id: msg.Id, - Type: "hello", - Hello: &proxy.HelloServerMessage{ - Version: "1.0", - SessionId: c.sessionId, - Server: &api.WelcomeServerMessage{ - Version: "1.0", - Country: c.server.country, - }, - }, - } - c.processMessage = c.processRegularMessage - return response, nil - } - - token, err := jwt.ParseWithClaims(msg.Hello.Token, &proxy.TokenClaims{}, func(token *jwt.Token) (any, error) { - claims, ok := token.Claims.(*proxy.TokenClaims) - if !assert.True(c.t, ok, "unsupported claims type: %+v", token.Claims) { - return nil, errors.New("unsupported claims type") - } - - key, found := c.server.Tokens[claims.Issuer] - if !assert.True(c.t, found) { - return nil, errors.New("no key found for issuer") - } - - return key, nil - }) - if assert.NoError(c.t, err) { - if assert.True(c.t, token.Valid) { - _, ok := token.Claims.(*proxy.TokenClaims) - assert.True(c.t, ok) - } - } - - response := &proxy.ServerMessage{ - Id: msg.Id, - Type: "hello", - Hello: &proxy.HelloServerMessage{ - Version: "1.0", - SessionId: c.sessionId, - Server: &api.WelcomeServerMessage{ - Version: "1.0", - Country: c.server.country, - }, - }, - } - c.processMessage = c.processRegularMessage - return response, nil -} - -func (c *testProxyServerClient) processRegularMessage(msg *proxy.ClientMessage) (*proxy.ServerMessage, error) { - var handler proxyServerClientHandler - switch msg.Type { - case "command": - handler = c.processCommandMessage - } - - if handler == nil { - response := msg.NewWrappedErrorServerMessage(fmt.Errorf("type \"%s\" is not implemented", msg.Type)) - return response, nil - } - - return handler(msg) -} - -func (c *testProxyServerClient) processCommandMessage(msg *proxy.ClientMessage) (*proxy.ServerMessage, error) { - var response *proxy.ServerMessage - switch msg.Command.Type { - case "create-publisher": - if strings.Contains(c.t.Name(), "ProxyPublisherTimeout") { - time.Sleep(2 * TimeoutTestTimeout) - defer c.server.Wakeup() - } - pub := c.server.createPublisher() - - if assert.NotNil(c.t, msg.Command.PublisherSettings) { - if assert.NotEqualValues(c.t, 0, msg.Command.PublisherSettings.Bitrate) { - assert.Equal(c.t, msg.Command.Bitrate, msg.Command.PublisherSettings.Bitrate) // nolint - } - assert.Equal(c.t, msg.Command.MediaTypes, msg.Command.PublisherSettings.MediaTypes) // nolint - if strings.Contains(c.t.Name(), "Codecs") { - assert.Equal(c.t, "opus,g722", msg.Command.PublisherSettings.AudioCodec) - assert.Equal(c.t, "vp9,vp8,av1", msg.Command.PublisherSettings.VideoCodec) - } else { - assert.Empty(c.t, msg.Command.PublisherSettings.AudioCodec) - assert.Empty(c.t, msg.Command.PublisherSettings.VideoCodec) - } - } - - response = &proxy.ServerMessage{ - Id: msg.Id, - Type: "command", - Command: &proxy.CommandServerMessage{ - Id: string(pub.id), - Bitrate: msg.Command.Bitrate, // nolint - }, - } - c.server.updateLoad(1) - case "delete-publisher": - if strings.Contains(c.t.Name(), "ProxyPublisherTimeout") { - defer c.server.Wakeup() - } - - if pub, found := c.server.deletePublisher(api.PublicSessionId(msg.Command.ClientId)); !found { - response = msg.NewWrappedErrorServerMessage(fmt.Errorf("publisher %s not found", msg.Command.ClientId)) - } else { - response = &proxy.ServerMessage{ - Id: msg.Id, - Type: "command", - Command: &proxy.CommandServerMessage{ - Id: string(pub.id), - }, - } - c.server.updateLoad(-1) - } - case "create-subscriber": - var pub *testProxyServerPublisher - if msg.Command.RemoteUrl != "" { - for _, server := range c.server.Servers { - if server.URL() != msg.Command.RemoteUrl { - continue - } - - token, err := jwt.ParseWithClaims(msg.Command.RemoteToken, &proxy.TokenClaims{}, func(token *jwt.Token) (any, error) { - claims, ok := token.Claims.(*proxy.TokenClaims) - if !assert.True(c.t, ok, "unsupported claims type: %+v", token.Claims) { - return nil, errors.New("unsupported claims type") - } - - key, found := server.getToken(claims.Issuer) - if !assert.True(c.t, found) { - return nil, errors.New("no key found for issuer") - } - - return key, nil - }) - if assert.NoError(c.t, err) { - if claims, ok := token.Claims.(*proxy.TokenClaims); assert.True(c.t, token.Valid) && assert.True(c.t, ok) { - assert.EqualValues(c.t, msg.Command.PublisherId, claims.Subject) - } - } - - pub = server.getPublisher(msg.Command.PublisherId) - break - } - } else { - pub = c.server.getPublisher(msg.Command.PublisherId) - } - - if pub == nil { - response = msg.NewWrappedErrorServerMessage(fmt.Errorf("publisher %s not found", msg.Command.PublisherId)) - } else { - if strings.Contains(c.t.Name(), "ProxySubscriberTimeout") { - time.Sleep(2 * TimeoutTestTimeout) - defer c.server.Wakeup() - } - sub := c.server.createSubscriber(pub) - response = &proxy.ServerMessage{ - Id: msg.Id, - Type: "command", - Command: &proxy.CommandServerMessage{ - Id: sub.id, - Sid: sub.sid, - }, - } - c.server.updateLoad(1) - } - case "delete-subscriber": - if strings.Contains(c.t.Name(), "ProxySubscriberTimeout") { - defer c.server.Wakeup() - } - if sub, found := c.server.deleteSubscriber(msg.Command.ClientId); !found { - response = msg.NewWrappedErrorServerMessage(fmt.Errorf("subscriber %s not found", msg.Command.ClientId)) - } else { - if msg.Command.RemoteUrl != sub.remoteUrl { - response = msg.NewWrappedErrorServerMessage(fmt.Errorf("remote subscriber %s not found", msg.Command.ClientId)) - return response, nil - } - - response = &proxy.ServerMessage{ - Id: msg.Id, - Type: "command", - Command: &proxy.CommandServerMessage{ - Id: sub.id, - }, - } - c.server.updateLoad(-1) - } - } - if response == nil { - response = msg.NewWrappedErrorServerMessage(fmt.Errorf("command \"%s\" is not implemented", msg.Command.Type)) - } - - return response, nil -} - -func (c *testProxyServerClient) Close() { - c.mu.Lock() - defer c.mu.Unlock() - - if c.ws != nil { - c.ws.Close() - c.ws = nil - } -} - -func (c *testProxyServerClient) handleSendMessageError(fmt string, msg *proxy.ServerMessage, err error) { - c.t.Helper() - - if !errors.Is(err, websocket.ErrCloseSent) || msg.Type != "event" || msg.Event.Type != "update-load" { - assert.Fail(c.t, "error while sending message", fmt, msg, err) - } -} - -func (c *testProxyServerClient) SendMessage(msg *proxy.ServerMessage) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.ws == nil { - return - } - - data, err := json.Marshal(msg) - if err != nil { - c.handleSendMessageError("error marshalling %+v: %s", msg, err) - return - } - - w, err := c.ws.NextWriter(websocket.TextMessage) - if err != nil { - c.handleSendMessageError("error creating writer for %+v: %s", msg, err) - return - } - - if _, err := w.Write(data); err != nil { - c.handleSendMessageError("error sending %+v: %s", msg, err) - return - } - - if err := w.Close(); err != nil { - c.handleSendMessageError("error during close of sending %+v: %s", msg, err) - } - - if msg.CloseAfterSend(nil) { - c.ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) // nolint - c.ws.Close() - } -} - -func (c *testProxyServerClient) run() { - defer func() { - c.mu.Lock() - defer c.mu.Unlock() - - c.server.expireSession(30*time.Second, c) - c.ws = nil - }() - c.processMessage = c.processHello - assert := assert.New(c.t) - for { - c.mu.Lock() - ws := c.ws - c.mu.Unlock() - if ws == nil { - break - } - - msgType, reader, err := ws.NextReader() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { - assert.NoError(err) - } - return - } - - body, err := io.ReadAll(reader) - if !assert.NoError(err) { - continue - } - - if !assert.Equal(websocket.TextMessage, msgType, "unexpected message type for %s", string(body)) { - continue - } - - var msg proxy.ClientMessage - if err := json.Unmarshal(body, &msg); !assert.NoError(err, "could not decode message %s", string(body)) { - continue - } - - if err := msg.CheckValid(); !assert.NoError(err, "invalid message %s", string(body)) { - continue - } - - response, err := c.processMessage(&msg) - if !assert.NoError(err) { - continue - } - - c.SendMessage(response) - if response.Type == "hello" { - c.server.sendLoad(c) - } - } -} - -type TestProxyServerHandler struct { - t *testing.T - - url string - server *httptest.Server - Servers []ProxyTestServer - Tokens map[string]*rsa.PublicKey - upgrader *websocket.Upgrader - country geoip.Country - - mu sync.Mutex - load atomic.Uint64 - incoming atomic.Pointer[float64] - outgoing atomic.Pointer[float64] - // +checklocks:mu - clients map[api.PublicSessionId]*testProxyServerClient - // +checklocks:mu - publishers map[api.PublicSessionId]*testProxyServerPublisher - // +checklocks:mu - subscribers map[string]*testProxyServerSubscriber - - wakeupChan chan struct{} -} - -func (h *TestProxyServerHandler) Start() { - h.server.Start() - h.url = h.server.URL -} - -func (h *TestProxyServerHandler) URL() string { - return h.url -} - -func (h *TestProxyServerHandler) SetURL(url string) { - h.url = url -} - -func (h *TestProxyServerHandler) Listener() net.Listener { - return h.server.Listener -} - -func (h *TestProxyServerHandler) SetListener(listener net.Listener) { - h.server.Listener = listener -} - -func (h *TestProxyServerHandler) SetServers(servers []ProxyTestServer) { - h.Servers = servers -} - -func (h *TestProxyServerHandler) SetToken(tokenId string, key *rsa.PublicKey) { - h.Tokens[tokenId] = key -} - -func (h *TestProxyServerHandler) getToken(tokenId string) (key *rsa.PublicKey, found bool) { - key, found = h.Tokens[tokenId] - return -} - -func (h *TestProxyServerHandler) createPublisher() *testProxyServerPublisher { - h.mu.Lock() - defer h.mu.Unlock() - pub := &testProxyServerPublisher{ - id: api.PublicSessionId(internal.RandomString(32)), - } - - for { - if _, found := h.publishers[pub.id]; !found { - break - } - - pub.id = api.PublicSessionId(internal.RandomString(32)) - } - h.publishers[pub.id] = pub - return pub -} - -func (h *TestProxyServerHandler) getPublisher(id api.PublicSessionId) *testProxyServerPublisher { - h.mu.Lock() - defer h.mu.Unlock() - - return h.publishers[id] -} - -func (h *TestProxyServerHandler) deletePublisher(id api.PublicSessionId) (*testProxyServerPublisher, bool) { - h.mu.Lock() - defer h.mu.Unlock() - - pub, found := h.publishers[id] - if !found { - return nil, false - } - - delete(h.publishers, id) - return pub, true -} - -func (h *TestProxyServerHandler) createSubscriber(pub *testProxyServerPublisher) *testProxyServerSubscriber { - h.mu.Lock() - defer h.mu.Unlock() - - sub := &testProxyServerSubscriber{ - id: internal.RandomString(32), - sid: internal.RandomString(8), - pub: pub, - } - - for { - if _, found := h.subscribers[sub.id]; !found { - break - } - - sub.id = internal.RandomString(32) - } - h.subscribers[sub.id] = sub - return sub -} - -func (h *TestProxyServerHandler) deleteSubscriber(id string) (*testProxyServerSubscriber, bool) { - h.mu.Lock() - defer h.mu.Unlock() - - sub, found := h.subscribers[id] - if !found { - return nil, false - } - - delete(h.subscribers, id) - return sub, true -} - -func (h *TestProxyServerHandler) UpdateBandwidth(incoming float64, outgoing float64) { - h.incoming.Store(&incoming) - h.outgoing.Store(&outgoing) - - h.mu.Lock() - defer h.mu.Unlock() - - msg := h.getLoadMessage(h.load.Load()) - for _, c := range h.clients { - c.SendMessage(msg) - } -} - -func (h *TestProxyServerHandler) Clear(incoming bool, outgoing bool) { - if incoming { - h.incoming.Store(nil) - } - if outgoing { - h.outgoing.Store(nil) - } - - h.mu.Lock() - defer h.mu.Unlock() - - msg := h.getLoadMessage(h.load.Load()) - for _, c := range h.clients { - c.SendMessage(msg) - } -} - -func (h *TestProxyServerHandler) getLoadMessage(load uint64) *proxy.ServerMessage { - msg := &proxy.ServerMessage{ - Type: "event", - Event: &proxy.EventServerMessage{ - Type: "update-load", - Load: load, - }, - } - - incoming := h.incoming.Load() - outgoing := h.outgoing.Load() - if incoming != nil || outgoing != nil { - msg.Event.Bandwidth = &proxy.EventServerBandwidth{ - Incoming: incoming, - Outgoing: outgoing, - } - } - return msg -} - -func (h *TestProxyServerHandler) updateLoad(delta int64) { - if delta == 0 { - return - } - - var load uint64 - if delta > 0 { - load = h.load.Add(uint64(delta)) - } else { - load = h.load.Add(^uint64(delta - 1)) - } - - h.mu.Lock() - defer h.mu.Unlock() - - msg := h.getLoadMessage(load) - for _, c := range h.clients { - c.SendMessage(msg) - } -} - -func (h *TestProxyServerHandler) sendLoad(c *testProxyServerClient) { - msg := h.getLoadMessage(h.load.Load()) - c.SendMessage(msg) -} - -func (h *TestProxyServerHandler) expireSession(timeout time.Duration, client *testProxyServerClient) { - timer := time.AfterFunc(timeout, func() { - h.removeClient(client) - }) - - h.t.Cleanup(func() { - timer.Stop() - }) -} - -func (h *TestProxyServerHandler) removeClient(client *testProxyServerClient) { - h.mu.Lock() - defer h.mu.Unlock() - - delete(h.clients, client.sessionId) -} - -func (h *TestProxyServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ws, err := h.upgrader.Upgrade(w, r, nil) - if !assert.NoError(h.t, err) { - return - } - - client := &testProxyServerClient{ - t: h.t, - server: h, - ws: ws, - sessionId: api.PublicSessionId(internal.RandomString(32)), - } - - h.setClient(client.sessionId, client) - - go client.run() -} - -func (h *TestProxyServerHandler) getClient(sessionId api.PublicSessionId) *testProxyServerClient { - h.mu.Lock() - defer h.mu.Unlock() - - return h.clients[sessionId] -} - -func (h *TestProxyServerHandler) setClient(sessionId api.PublicSessionId, client *testProxyServerClient) { - h.mu.Lock() - defer h.mu.Unlock() - - if prev, found := h.clients[sessionId]; found { - prev.SendMessage(&proxy.ServerMessage{ - Type: "bye", - Bye: &proxy.ByeServerMessage{ - Reason: "session_resumed", - }, - }) - prev.Close() - } - - h.clients[sessionId] = client -} - -func (h *TestProxyServerHandler) Wakeup() { - h.wakeupChan <- struct{}{} -} - -func (h *TestProxyServerHandler) WaitForWakeup(ctx context.Context) error { - select { - case <-h.wakeupChan: - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -func (h *TestProxyServerHandler) GetSingleClient() TestProxyClient { - h.mu.Lock() - defer h.mu.Unlock() - - for _, c := range h.clients { - return c - } - - return nil -} - -func (h *TestProxyServerHandler) ClearClients() { - h.mu.Lock() - defer h.mu.Unlock() - - clear(h.clients) -} - -func NewProxyServerForTest(t *testing.T, country geoip.Country) *TestProxyServerHandler { - t.Helper() - - upgrader := websocket.Upgrader{} - proxyHandler := &TestProxyServerHandler{ - t: t, - Tokens: make(map[string]*rsa.PublicKey), - upgrader: &upgrader, - country: country, - clients: make(map[api.PublicSessionId]*testProxyServerClient), - publishers: make(map[api.PublicSessionId]*testProxyServerPublisher), - subscribers: make(map[string]*testProxyServerSubscriber), - wakeupChan: make(chan struct{}), - } - server := httptest.NewUnstartedServer(proxyHandler) - if !strings.Contains(t.Name(), "DnsDiscovery") { - server.Start() - } - proxyHandler.server = server - proxyHandler.url = server.URL - t.Cleanup(func() { - server.Close() - proxyHandler.mu.Lock() - defer proxyHandler.mu.Unlock() - for _, c := range proxyHandler.clients { - c.Close() - } - }) - - return proxyHandler -} diff --git a/sfu/test/sfu.go b/sfu/test/sfu.go deleted file mode 100644 index 6b64edc..0000000 --- a/sfu/test/sfu.go +++ /dev/null @@ -1,329 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2019 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "context" - "errors" - "fmt" - "maps" - "net" - "sync" - "sync/atomic" - "testing" - - "github.com/dlintw/goconf" - "github.com/stretchr/testify/assert" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - logtest "github.com/strukturag/nextcloud-spreed-signaling/v2/log/test" - "github.com/strukturag/nextcloud-spreed-signaling/v2/mock" - "github.com/strukturag/nextcloud-spreed-signaling/v2/sfu" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" -) - -var ( - TestMaxBitrateScreen = api.BandwidthFromBits(12345678) - TestMaxBitrateVideo = api.BandwidthFromBits(23456789) -) - -type SFU struct { - t *testing.T - mu sync.Mutex - // +checklocks:mu - publishers map[api.PublicSessionId]*SFUPublisher - // +checklocks:mu - subscribers map[string]*SFUSubscriber - - maxStreamBitrate api.AtomicBandwidth - maxScreenBitrate api.AtomicBandwidth -} - -func NewSFU(t *testing.T) *SFU { - return &SFU{ - t: t, - - publishers: make(map[api.PublicSessionId]*SFUPublisher), - subscribers: make(map[string]*SFUSubscriber), - } -} - -func (m *SFU) GetBandwidthLimits() (api.Bandwidth, api.Bandwidth) { - return m.maxStreamBitrate.Load(), m.maxScreenBitrate.Load() -} - -func (m *SFU) SetBandwidthLimits(maxStreamBitrate api.Bandwidth, maxScreenBitrate api.Bandwidth) { - m.maxStreamBitrate.Store(maxStreamBitrate) - m.maxScreenBitrate.Store(maxScreenBitrate) -} - -func (m *SFU) Start(ctx context.Context) error { - return nil -} - -func (m *SFU) Stop() { -} - -func (m *SFU) Reload(config *goconf.ConfigFile) { -} - -func (m *SFU) SetOnConnected(f func()) { -} - -func (m *SFU) SetOnDisconnected(f func()) { -} - -func (m *SFU) GetStats() any { - return nil -} - -func (m *SFU) GetServerInfoSfu() *talk.BackendServerInfoSfu { - return nil -} - -func (m *SFU) NewPublisher(ctx context.Context, listener sfu.Listener, id api.PublicSessionId, sid string, streamType sfu.StreamType, settings sfu.NewPublisherSettings, initiator sfu.Initiator) (sfu.Publisher, error) { - var maxBitrate api.Bandwidth - if streamType == sfu.StreamTypeScreen { - maxBitrate = TestMaxBitrateScreen - } else { - maxBitrate = TestMaxBitrateVideo - } - publisherSettings := settings - bitrate := publisherSettings.Bitrate - if bitrate <= 0 || bitrate > maxBitrate { - publisherSettings.Bitrate = maxBitrate - } - pub := &SFUPublisher{ - SFUClient: SFUClient{ - t: m.t, - id: string(id), - sid: sid, - streamType: streamType, - }, - - settings: publisherSettings, - } - - m.mu.Lock() - defer m.mu.Unlock() - - m.publishers[id] = pub - return pub, nil -} - -func (m *SFU) GetPublishers() map[api.PublicSessionId]*SFUPublisher { - m.mu.Lock() - defer m.mu.Unlock() - - result := maps.Clone(m.publishers) - return result -} - -func (m *SFU) GetPublisher(id api.PublicSessionId) *SFUPublisher { - m.mu.Lock() - defer m.mu.Unlock() - - return m.publishers[id] -} - -func (m *SFU) GetSubscriber(id api.PublicSessionId, streamType sfu.StreamType) *SFUSubscriber { - m.mu.Lock() - defer m.mu.Unlock() - - key := fmt.Sprintf("%s|%s", id, streamType) - return m.subscribers[key] -} - -func (m *SFU) NewSubscriber(ctx context.Context, listener sfu.Listener, publisher api.PublicSessionId, streamType sfu.StreamType, initiator sfu.Initiator) (sfu.Subscriber, error) { - m.mu.Lock() - defer m.mu.Unlock() - - pub := m.publishers[publisher] - if pub == nil { - return nil, errors.New("waiting for publisher not implemented yet") - } - - id := internal.RandomString(8) - sub := &SFUSubscriber{ - SFUClient: SFUClient{ - t: m.t, - id: id, - streamType: streamType, - }, - - publisher: pub, - } - key := fmt.Sprintf("%s|%s", publisher, streamType) - assert.Empty(m.t, m.subscribers[key], "duplicate subscriber") - m.subscribers[key] = sub - return sub, nil -} - -type SFUClient struct { - t *testing.T - closed atomic.Bool - - id string - sid string - streamType sfu.StreamType -} - -func (c *SFUClient) Id() string { - return c.id -} - -func (c *SFUClient) Sid() string { - return c.sid -} - -func (c *SFUClient) StreamType() sfu.StreamType { - return c.streamType -} - -func (c *SFUClient) MaxBitrate() api.Bandwidth { - return 0 -} - -func (c *SFUClient) Close(ctx context.Context) { - if c.closed.CompareAndSwap(false, true) { - logger := logtest.NewLoggerForTest(c.t) - logger.Printf("Close SFU client %s", c.id) - } -} - -func (c *SFUClient) IsClosed() bool { - return c.closed.Load() -} - -type SFUPublisher struct { - SFUClient - - settings sfu.NewPublisherSettings - - sdp string -} - -func (p *SFUPublisher) Settings() sfu.NewPublisherSettings { - return p.settings -} - -func (p *SFUPublisher) PublisherId() api.PublicSessionId { - return api.PublicSessionId(p.id) -} - -func (p *SFUPublisher) HasMedia(mt sfu.MediaType) bool { - return (p.settings.MediaTypes & mt) == mt -} - -func (p *SFUPublisher) SetMedia(mt sfu.MediaType) { - p.settings.MediaTypes = mt -} - -func (p *SFUPublisher) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { - go func() { - if p.IsClosed() { - callback(errors.New("already closed"), nil) - return - } - - switch data.Type { - case "offer": - sdp := data.Payload["sdp"] - if sdp, ok := sdp.(string); ok { - p.sdp = sdp - switch sdp { - case mock.MockSdpOfferAudioOnly: - callback(nil, api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioOnly, - }) - return - case mock.MockSdpOfferAudioAndVideo: - callback(nil, api.StringMap{ - "type": "answer", - "sdp": mock.MockSdpAnswerAudioAndVideo, - }) - return - } - } - callback(fmt.Errorf("offer payload %+v is not implemented", data.Payload), nil) - default: - callback(fmt.Errorf("message type %s is not implemented", data.Type), nil) - } - }() -} - -func (p *SFUPublisher) GetStreams(ctx context.Context) ([]sfu.PublisherStream, error) { - return nil, errors.New("not implemented") -} - -func (p *SFUPublisher) PublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error { - return errors.New("remote publishing not supported") -} - -func (p *SFUPublisher) UnpublishRemote(ctx context.Context, remoteId api.PublicSessionId, hostname string, port int, rtcpPort int) error { - return errors.New("remote publishing not supported") -} - -func (p *SFUPublisher) GetConnectionURL() (string, net.IP) { - return "https://proxy.domain.invalid", net.ParseIP("10.20.30.40") -} - -type SFUSubscriber struct { - SFUClient - - publisher *SFUPublisher -} - -func (s *SFUSubscriber) Publisher() api.PublicSessionId { - return s.publisher.PublisherId() -} - -func (s *SFUSubscriber) SendMessage(ctx context.Context, message *api.MessageClientMessage, data *api.MessageClientMessageData, callback func(error, api.StringMap)) { - go func() { - if s.IsClosed() { - callback(errors.New("already closed"), nil) - return - } - - switch data.Type { - case "requestoffer": - fallthrough - case "sendoffer": - sdp := s.publisher.sdp - if sdp == "" { - callback(errors.New("publisher not sending (no SDP)"), nil) - return - } - - callback(nil, api.StringMap{ - "type": "offer", - "sdp": sdp, - }) - case "answer": - callback(nil, nil) - default: - callback(fmt.Errorf("message type %s is not implemented", data.Type), nil) - } - }() -} diff --git a/single_notifier.go b/single_notifier.go new file mode 100644 index 0000000..921542a --- /dev/null +++ b/single_notifier.go @@ -0,0 +1,126 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "sync" +) + +type SingleWaiter struct { + root bool + ch chan struct{} + once sync.Once +} + +func newSingleWaiter() *SingleWaiter { + return &SingleWaiter{ + root: true, + ch: make(chan struct{}), + } +} + +func (w *SingleWaiter) subWaiter() *SingleWaiter { + return &SingleWaiter{ + ch: w.ch, + } +} + +func (w *SingleWaiter) Wait(ctx context.Context) error { + select { + case <-w.ch: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (w *SingleWaiter) cancel() { + if !w.root { + return + } + + w.once.Do(func() { + close(w.ch) + }) +} + +type SingleNotifier struct { + sync.Mutex + + waiter *SingleWaiter + waiters map[*SingleWaiter]bool +} + +func (n *SingleNotifier) NewWaiter() *SingleWaiter { + n.Lock() + defer n.Unlock() + + if n.waiter == nil { + n.waiter = newSingleWaiter() + } + + if n.waiters == nil { + n.waiters = make(map[*SingleWaiter]bool) + } + + w := n.waiter.subWaiter() + n.waiters[w] = true + return w +} + +func (n *SingleNotifier) Reset() { + n.Lock() + defer n.Unlock() + + if n.waiter != nil { + n.waiter.cancel() + n.waiter = nil + } + n.waiters = nil +} + +func (n *SingleNotifier) Release(w *SingleWaiter) { + n.Lock() + defer n.Unlock() + + if _, found := n.waiters[w]; found { + delete(n.waiters, w) + if len(n.waiters) == 0 { + n.waiters = nil + if n.waiter != nil { + n.waiter.cancel() + n.waiter = nil + } + } + } +} + +func (n *SingleNotifier) Notify() { + n.Lock() + defer n.Unlock() + + if n.waiter != nil { + n.waiter.cancel() + } + n.waiters = nil +} diff --git a/single_notifier_test.go b/single_notifier_test.go new file mode 100644 index 0000000..7b95beb --- /dev/null +++ b/single_notifier_test.go @@ -0,0 +1,141 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2022 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSingleNotifierNoWaiter(t *testing.T) { + var notifier SingleNotifier + + // Notifications can be sent even if no waiter exists. + notifier.Notify() +} + +func TestSingleNotifierSimple(t *testing.T) { + var notifier SingleNotifier + + var wg sync.WaitGroup + wg.Add(1) + + waiter := notifier.NewWaiter() + defer notifier.Release(waiter) + + go func() { + defer wg.Done() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + assert.NoError(t, waiter.Wait(ctx)) + }() + + notifier.Notify() + wg.Wait() +} + +func TestSingleNotifierMultiNotify(t *testing.T) { + var notifier SingleNotifier + + waiter := notifier.NewWaiter() + defer notifier.Release(waiter) + + notifier.Notify() + // The second notification will be ignored while the first is still pending. + notifier.Notify() +} + +func TestSingleNotifierWaitClosed(t *testing.T) { + var notifier SingleNotifier + + waiter := notifier.NewWaiter() + notifier.Release(waiter) + + assert.NoError(t, waiter.Wait(context.Background())) +} + +func TestSingleNotifierWaitClosedMulti(t *testing.T) { + var notifier SingleNotifier + + waiter1 := notifier.NewWaiter() + waiter2 := notifier.NewWaiter() + notifier.Release(waiter1) + notifier.Release(waiter2) + + assert.NoError(t, waiter1.Wait(context.Background())) + assert.NoError(t, waiter2.Wait(context.Background())) +} + +func TestSingleNotifierResetWillNotify(t *testing.T) { + var notifier SingleNotifier + + var wg sync.WaitGroup + wg.Add(1) + + waiter := notifier.NewWaiter() + defer notifier.Release(waiter) + + go func() { + defer wg.Done() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + assert.NoError(t, waiter.Wait(ctx)) + }() + + notifier.Reset() + wg.Wait() +} + +func TestSingleNotifierDuplicate(t *testing.T) { + t.Parallel() + var notifier SingleNotifier + var wgStart sync.WaitGroup + var wgEnd sync.WaitGroup + + for i := 0; i < 2; i++ { + wgStart.Add(1) + wgEnd.Add(1) + + go func() { + defer wgEnd.Done() + waiter := notifier.NewWaiter() + defer notifier.Release(waiter) + + // Goroutine has created the waiter and is ready. + wgStart.Done() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + assert.NoError(t, waiter.Wait(ctx)) + }() + } + + wgStart.Wait() + + time.Sleep(100 * time.Millisecond) + notifier.Notify() + wgEnd.Wait() +} diff --git a/server/stats_prometheus.go b/stats_prometheus.go similarity index 76% rename from server/stats_prometheus.go rename to stats_prometheus.go index 9b6b973..93af6af 100644 --- a/server/stats_prometheus.go +++ b/stats_prometheus.go @@ -19,12 +19,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" ) var ( @@ -40,6 +38,22 @@ var ( } ) -func RegisterStats() { - metrics.RegisterAll(signalingStats...) +func registerAll(cs ...prometheus.Collector) { + for _, c := range cs { + if err := prometheus.DefaultRegisterer.Register(c); err != nil { + if _, ok := err.(prometheus.AlreadyRegisteredError); !ok { + panic(err) + } + } + } +} + +func unregisterAll(cs ...prometheus.Collector) { + for _, c := range cs { + prometheus.Unregister(c) + } +} + +func RegisterStats() { + registerAll(signalingStats...) } diff --git a/metrics/test/metrics.go b/stats_prometheus_test.go similarity index 58% rename from metrics/test/metrics.go rename to stats_prometheus_test.go index f7f126c..626777a 100644 --- a/metrics/test/metrics.go +++ b/stats_prometheus_test.go @@ -19,9 +19,12 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package test +package signaling import ( + "fmt" + "runtime" + "strings" "testing" "github.com/prometheus/client_golang/prometheus" @@ -29,44 +32,38 @@ import ( "github.com/stretchr/testify/assert" ) -func ResetStatsValue[T prometheus.Gauge](t *testing.T, collector T) { - // Make sure test is not executed with "t.Parallel()" - t.Setenv("PARALLEL_CHECK", "1") - - collector.Set(0) - t.Cleanup(func() { - collector.Set(0) - }) -} - -func AssertCollectorChangeBy(t *testing.T, collector prometheus.Collector, delta float64) { - t.Helper() - - ch := make(chan *prometheus.Desc, 1) - collector.Describe(ch) - desc := <-ch - - before := testutil.ToFloat64(collector) - t.Cleanup(func() { - t.Helper() - - after := testutil.ToFloat64(collector) - assert.InEpsilon(t, delta, after-before, 0.0001, "failed for %s", desc) - }) -} - -func CheckStatsValue(t *testing.T, collector prometheus.Collector, value float64) { - // Make sure test is not executed with "t.Parallel()" - t.Setenv("PARALLEL_CHECK", "1") - +func checkStatsValue(t *testing.T, collector prometheus.Collector, value float64) { ch := make(chan *prometheus.Desc, 1) collector.Describe(ch) desc := <-ch v := testutil.ToFloat64(collector) - assert.InDelta(t, value, v, 0.0001, "unexpected value for %s", desc) + if v != value { + assert := assert.New(t) + pc := make([]uintptr, 10) + n := runtime.Callers(2, pc) + if n == 0 { + assert.Fail("Expected value %f for %s, got %f", value, desc, v) + return + } + + pc = pc[:n] + frames := runtime.CallersFrames(pc) + stack := "" + for { + frame, more := frames.Next() + if !strings.Contains(frame.File, "nextcloud-spreed-signaling") { + break + } + stack += fmt.Sprintf("%s:%d\n", frame.File, frame.Line) + if !more { + break + } + } + assert.Fail("Expected value %f for %s, got %f at\n%s", value, desc, v, stack) + } } -func CollectAndLint(t *testing.T, collectors ...prometheus.Collector) { +func collectAndLint(t *testing.T, collectors ...prometheus.Collector) { assert := assert.New(t) for _, collector := range collectors { problems, err := testutil.CollectAndLint(collector) @@ -75,7 +72,7 @@ func CollectAndLint(t *testing.T, collectors ...prometheus.Collector) { } for _, problem := range problems { - assert.Fail("Problem with metric", "%s: %s", problem.Metric, problem.Text) + assert.Fail("Problem with %s: %s", problem.Metric, problem.Text) } } } diff --git a/grpc/syscallconn.go b/syscallconn.go similarity index 99% rename from grpc/syscallconn.go rename to syscallconn.go index 6f7a13d..db33e69 100644 --- a/grpc/syscallconn.go +++ b/syscallconn.go @@ -16,7 +16,7 @@ * */ -package grpc +package signaling import ( "net" diff --git a/talk/api_easyjson.go b/talk/api_easyjson.go deleted file mode 100644 index d4703a3..0000000 --- a/talk/api_easyjson.go +++ /dev/null @@ -1,4891 +0,0 @@ -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. - -package talk - -import ( - json "encoding/json" - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" - api "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - etcd "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - geoip "github.com/strukturag/nextcloud-spreed-signaling/v2/geoip" - time "time" -) - -// suppress unused package warning -var ( - _ *json.RawMessage - _ *jlexer.Lexer - _ *jwriter.Writer - _ easyjson.Marshaler -) - -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(in *jlexer.Lexer, out *TurnCredentials) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "username": - if in.IsNull() { - in.Skip() - } else { - out.Username = string(in.String()) - } - case "password": - if in.IsNull() { - in.Skip() - } else { - out.Password = string(in.String()) - } - case "ttl": - if in.IsNull() { - in.Skip() - } else { - out.TTL = int64(in.Int64()) - } - case "uris": - if in.IsNull() { - in.Skip() - out.URIs = nil - } else { - in.Delim('[') - if out.URIs == nil { - if !in.IsDelim(']') { - out.URIs = make([]string, 0, 4) - } else { - out.URIs = []string{} - } - } else { - out.URIs = (out.URIs)[:0] - } - for !in.IsDelim(']') { - var v1 string - if in.IsNull() { - in.Skip() - } else { - v1 = string(in.String()) - } - out.URIs = append(out.URIs, v1) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(out *jwriter.Writer, in TurnCredentials) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"username\":" - out.RawString(prefix[1:]) - out.String(string(in.Username)) - } - { - const prefix string = ",\"password\":" - out.RawString(prefix) - out.String(string(in.Password)) - } - { - const prefix string = ",\"ttl\":" - out.RawString(prefix) - out.Int64(int64(in.TTL)) - } - { - const prefix string = ",\"uris\":" - out.RawString(prefix) - if in.URIs == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v2, v3 := range in.URIs { - if v2 > 0 { - out.RawByte(',') - } - out.String(string(v3)) - } - out.RawByte(']') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v TurnCredentials) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v TurnCredentials) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *TurnCredentials) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *TurnCredentials) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(in *jlexer.Lexer, out *RoomSessionData) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(out *jwriter.Writer, in RoomSessionData) { - out.RawByte('{') - first := true - _ = first - if in.UserId != "" { - const prefix string = ",\"userid\":" - first = false - out.RawString(prefix[1:]) - out.String(string(in.UserId)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v RoomSessionData) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v RoomSessionData) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *RoomSessionData) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *RoomSessionData) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(in *jlexer.Lexer, out *BackendServerRoomResponse) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } - case "dialout": - if in.IsNull() { - in.Skip() - out.Dialout = nil - } else { - if out.Dialout == nil { - out.Dialout = new(BackendRoomDialoutResponse) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Dialout).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(out *jwriter.Writer, in BackendServerRoomResponse) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"type\":" - out.RawString(prefix[1:]) - out.String(string(in.Type)) - } - if in.Dialout != nil { - const prefix string = ",\"dialout\":" - out.RawString(prefix) - (*in.Dialout).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerRoomResponse) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerRoomResponse) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerRoomResponse) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerRoomResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk3(in *jlexer.Lexer, out *BackendServerRoomRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } - case "invite": - if in.IsNull() { - in.Skip() - out.Invite = nil - } else { - if out.Invite == nil { - out.Invite = new(BackendRoomInviteRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Invite).UnmarshalEasyJSON(in) - } - } - case "disinvite": - if in.IsNull() { - in.Skip() - out.Disinvite = nil - } else { - if out.Disinvite == nil { - out.Disinvite = new(BackendRoomDisinviteRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Disinvite).UnmarshalEasyJSON(in) - } - } - case "update": - if in.IsNull() { - in.Skip() - out.Update = nil - } else { - if out.Update == nil { - out.Update = new(BackendRoomUpdateRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Update).UnmarshalEasyJSON(in) - } - } - case "delete": - if in.IsNull() { - in.Skip() - out.Delete = nil - } else { - if out.Delete == nil { - out.Delete = new(BackendRoomDeleteRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Delete).UnmarshalEasyJSON(in) - } - } - case "incall": - if in.IsNull() { - in.Skip() - out.InCall = nil - } else { - if out.InCall == nil { - out.InCall = new(BackendRoomInCallRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.InCall).UnmarshalEasyJSON(in) - } - } - case "participants": - if in.IsNull() { - in.Skip() - out.Participants = nil - } else { - if out.Participants == nil { - out.Participants = new(BackendRoomParticipantsRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Participants).UnmarshalEasyJSON(in) - } - } - case "message": - if in.IsNull() { - in.Skip() - out.Message = nil - } else { - if out.Message == nil { - out.Message = new(BackendRoomMessageRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Message).UnmarshalEasyJSON(in) - } - } - case "switchto": - if in.IsNull() { - in.Skip() - out.SwitchTo = nil - } else { - if out.SwitchTo == nil { - out.SwitchTo = new(BackendRoomSwitchToMessageRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.SwitchTo).UnmarshalEasyJSON(in) - } - } - case "dialout": - if in.IsNull() { - in.Skip() - out.Dialout = nil - } else { - if out.Dialout == nil { - out.Dialout = new(BackendRoomDialoutRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Dialout).UnmarshalEasyJSON(in) - } - } - case "transient": - if in.IsNull() { - in.Skip() - out.Transient = nil - } else { - if out.Transient == nil { - out.Transient = new(BackendRoomTransientRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Transient).UnmarshalEasyJSON(in) - } - } - case "received": - if in.IsNull() { - in.Skip() - } else { - out.ReceivedTime = int64(in.Int64()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk3(out *jwriter.Writer, in BackendServerRoomRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"type\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.Type)) - } - if in.Invite != nil { - const prefix string = ",\"invite\":" - out.RawString(prefix) - (*in.Invite).MarshalEasyJSON(out) - } - if in.Disinvite != nil { - const prefix string = ",\"disinvite\":" - out.RawString(prefix) - (*in.Disinvite).MarshalEasyJSON(out) - } - if in.Update != nil { - const prefix string = ",\"update\":" - out.RawString(prefix) - (*in.Update).MarshalEasyJSON(out) - } - if in.Delete != nil { - const prefix string = ",\"delete\":" - out.RawString(prefix) - (*in.Delete).MarshalEasyJSON(out) - } - if in.InCall != nil { - const prefix string = ",\"incall\":" - out.RawString(prefix) - (*in.InCall).MarshalEasyJSON(out) - } - if in.Participants != nil { - const prefix string = ",\"participants\":" - out.RawString(prefix) - (*in.Participants).MarshalEasyJSON(out) - } - if in.Message != nil { - const prefix string = ",\"message\":" - out.RawString(prefix) - (*in.Message).MarshalEasyJSON(out) - } - if in.SwitchTo != nil { - const prefix string = ",\"switchto\":" - out.RawString(prefix) - (*in.SwitchTo).MarshalEasyJSON(out) - } - if in.Dialout != nil { - const prefix string = ",\"dialout\":" - out.RawString(prefix) - (*in.Dialout).MarshalEasyJSON(out) - } - if in.Transient != nil { - const prefix string = ",\"transient\":" - out.RawString(prefix) - (*in.Transient).MarshalEasyJSON(out) - } - if in.ReceivedTime != 0 { - const prefix string = ",\"received\":" - out.RawString(prefix) - out.Int64(int64(in.ReceivedTime)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerRoomRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk3(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerRoomRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk3(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerRoomRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk3(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerRoomRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk3(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk4(in *jlexer.Lexer, out *BackendServerInfoVideoRoom) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "name": - if in.IsNull() { - in.Skip() - } else { - out.Name = string(in.String()) - } - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "author": - if in.IsNull() { - in.Skip() - } else { - out.Author = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk4(out *jwriter.Writer, in BackendServerInfoVideoRoom) { - out.RawByte('{') - first := true - _ = first - if in.Name != "" { - const prefix string = ",\"name\":" - first = false - out.RawString(prefix[1:]) - out.String(string(in.Name)) - } - if in.Version != "" { - const prefix string = ",\"version\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.Version)) - } - if in.Author != "" { - const prefix string = ",\"author\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.Author)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoVideoRoom) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk4(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoVideoRoom) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk4(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoVideoRoom) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk4(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoVideoRoom) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk4(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk5(in *jlexer.Lexer, out *BackendServerInfoSfuProxyBandwidth) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "incoming": - if in.IsNull() { - in.Skip() - out.Incoming = nil - } else { - if out.Incoming == nil { - out.Incoming = new(float64) - } - if in.IsNull() { - in.Skip() - } else { - *out.Incoming = float64(in.Float64()) - } - } - case "outgoing": - if in.IsNull() { - in.Skip() - out.Outgoing = nil - } else { - if out.Outgoing == nil { - out.Outgoing = new(float64) - } - if in.IsNull() { - in.Skip() - } else { - *out.Outgoing = float64(in.Float64()) - } - } - case "received": - if in.IsNull() { - in.Skip() - } else { - out.Received = api.Bandwidth(in.Uint64()) - } - case "sent": - if in.IsNull() { - in.Skip() - } else { - out.Sent = api.Bandwidth(in.Uint64()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk5(out *jwriter.Writer, in BackendServerInfoSfuProxyBandwidth) { - out.RawByte('{') - first := true - _ = first - if in.Incoming != nil { - const prefix string = ",\"incoming\":" - first = false - out.RawString(prefix[1:]) - out.Float64(float64(*in.Incoming)) - } - if in.Outgoing != nil { - const prefix string = ",\"outgoing\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Float64(float64(*in.Outgoing)) - } - if in.Received != 0 { - const prefix string = ",\"received\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Uint64(uint64(in.Received)) - } - if in.Sent != 0 { - const prefix string = ",\"sent\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Uint64(uint64(in.Sent)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoSfuProxyBandwidth) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk5(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoSfuProxyBandwidth) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk5(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoSfuProxyBandwidth) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk5(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoSfuProxyBandwidth) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk5(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk6(in *jlexer.Lexer, out *BackendServerInfoSfuProxy) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "url": - if in.IsNull() { - in.Skip() - } else { - out.Url = string(in.String()) - } - case "ip": - if in.IsNull() { - in.Skip() - } else { - out.IP = string(in.String()) - } - case "connected": - if in.IsNull() { - in.Skip() - } else { - out.Connected = bool(in.Bool()) - } - case "temporary": - if in.IsNull() { - in.Skip() - } else { - out.Temporary = bool(in.Bool()) - } - case "shutdown": - if in.IsNull() { - in.Skip() - out.Shutdown = nil - } else { - if out.Shutdown == nil { - out.Shutdown = new(bool) - } - if in.IsNull() { - in.Skip() - } else { - *out.Shutdown = bool(in.Bool()) - } - } - case "uptime": - if in.IsNull() { - in.Skip() - out.Uptime = nil - } else { - if out.Uptime == nil { - out.Uptime = new(time.Time) - } - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((*out.Uptime).UnmarshalJSON(data)) - } - } - } - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "features": - if in.IsNull() { - in.Skip() - out.Features = nil - } else { - in.Delim('[') - if out.Features == nil { - if !in.IsDelim(']') { - out.Features = make([]string, 0, 4) - } else { - out.Features = []string{} - } - } else { - out.Features = (out.Features)[:0] - } - for !in.IsDelim(']') { - var v4 string - if in.IsNull() { - in.Skip() - } else { - v4 = string(in.String()) - } - out.Features = append(out.Features, v4) - in.WantComma() - } - in.Delim(']') - } - case "country": - if in.IsNull() { - in.Skip() - } else { - out.Country = geoip.Country(in.String()) - } - case "load": - if in.IsNull() { - in.Skip() - out.Load = nil - } else { - if out.Load == nil { - out.Load = new(uint64) - } - if in.IsNull() { - in.Skip() - } else { - *out.Load = uint64(in.Uint64()) - } - } - case "bandwidth": - if in.IsNull() { - in.Skip() - out.Bandwidth = nil - } else { - if out.Bandwidth == nil { - out.Bandwidth = new(BackendServerInfoSfuProxyBandwidth) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Bandwidth).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk6(out *jwriter.Writer, in BackendServerInfoSfuProxy) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"url\":" - out.RawString(prefix[1:]) - out.String(string(in.Url)) - } - if in.IP != "" { - const prefix string = ",\"ip\":" - out.RawString(prefix) - out.String(string(in.IP)) - } - { - const prefix string = ",\"connected\":" - out.RawString(prefix) - out.Bool(bool(in.Connected)) - } - { - const prefix string = ",\"temporary\":" - out.RawString(prefix) - out.Bool(bool(in.Temporary)) - } - if in.Shutdown != nil { - const prefix string = ",\"shutdown\":" - out.RawString(prefix) - out.Bool(bool(*in.Shutdown)) - } - if in.Uptime != nil { - const prefix string = ",\"uptime\":" - out.RawString(prefix) - out.Raw((*in.Uptime).MarshalJSON()) - } - if in.Version != "" { - const prefix string = ",\"version\":" - out.RawString(prefix) - out.String(string(in.Version)) - } - if len(in.Features) != 0 { - const prefix string = ",\"features\":" - out.RawString(prefix) - { - out.RawByte('[') - for v5, v6 := range in.Features { - if v5 > 0 { - out.RawByte(',') - } - out.String(string(v6)) - } - out.RawByte(']') - } - } - if in.Country != "" { - const prefix string = ",\"country\":" - out.RawString(prefix) - out.String(string(in.Country)) - } - if in.Load != nil { - const prefix string = ",\"load\":" - out.RawString(prefix) - out.Uint64(uint64(*in.Load)) - } - if in.Bandwidth != nil { - const prefix string = ",\"bandwidth\":" - out.RawString(prefix) - (*in.Bandwidth).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoSfuProxy) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk6(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoSfuProxy) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk6(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoSfuProxy) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk6(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoSfuProxy) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk6(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk7(in *jlexer.Lexer, out *BackendServerInfoSfuJanus) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "url": - if in.IsNull() { - in.Skip() - } else { - out.Url = string(in.String()) - } - case "connected": - if in.IsNull() { - in.Skip() - } else { - out.Connected = bool(in.Bool()) - } - case "name": - if in.IsNull() { - in.Skip() - } else { - out.Name = string(in.String()) - } - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "author": - if in.IsNull() { - in.Skip() - } else { - out.Author = string(in.String()) - } - case "datachannels": - if in.IsNull() { - in.Skip() - out.DataChannels = nil - } else { - if out.DataChannels == nil { - out.DataChannels = new(bool) - } - if in.IsNull() { - in.Skip() - } else { - *out.DataChannels = bool(in.Bool()) - } - } - case "fulltrickle": - if in.IsNull() { - in.Skip() - out.FullTrickle = nil - } else { - if out.FullTrickle == nil { - out.FullTrickle = new(bool) - } - if in.IsNull() { - in.Skip() - } else { - *out.FullTrickle = bool(in.Bool()) - } - } - case "localip": - if in.IsNull() { - in.Skip() - } else { - out.LocalIP = string(in.String()) - } - case "ipv6": - if in.IsNull() { - in.Skip() - out.IPv6 = nil - } else { - if out.IPv6 == nil { - out.IPv6 = new(bool) - } - if in.IsNull() { - in.Skip() - } else { - *out.IPv6 = bool(in.Bool()) - } - } - case "videoroom": - if in.IsNull() { - in.Skip() - out.VideoRoom = nil - } else { - if out.VideoRoom == nil { - out.VideoRoom = new(BackendServerInfoVideoRoom) - } - if in.IsNull() { - in.Skip() - } else { - (*out.VideoRoom).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk7(out *jwriter.Writer, in BackendServerInfoSfuJanus) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"url\":" - out.RawString(prefix[1:]) - out.String(string(in.Url)) - } - { - const prefix string = ",\"connected\":" - out.RawString(prefix) - out.Bool(bool(in.Connected)) - } - if in.Name != "" { - const prefix string = ",\"name\":" - out.RawString(prefix) - out.String(string(in.Name)) - } - if in.Version != "" { - const prefix string = ",\"version\":" - out.RawString(prefix) - out.String(string(in.Version)) - } - if in.Author != "" { - const prefix string = ",\"author\":" - out.RawString(prefix) - out.String(string(in.Author)) - } - if in.DataChannels != nil { - const prefix string = ",\"datachannels\":" - out.RawString(prefix) - out.Bool(bool(*in.DataChannels)) - } - if in.FullTrickle != nil { - const prefix string = ",\"fulltrickle\":" - out.RawString(prefix) - out.Bool(bool(*in.FullTrickle)) - } - if in.LocalIP != "" { - const prefix string = ",\"localip\":" - out.RawString(prefix) - out.String(string(in.LocalIP)) - } - if in.IPv6 != nil { - const prefix string = ",\"ipv6\":" - out.RawString(prefix) - out.Bool(bool(*in.IPv6)) - } - if in.VideoRoom != nil { - const prefix string = ",\"videoroom\":" - out.RawString(prefix) - (*in.VideoRoom).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoSfuJanus) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk7(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoSfuJanus) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk7(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoSfuJanus) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk7(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoSfuJanus) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk7(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk8(in *jlexer.Lexer, out *BackendServerInfoSfu) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "mode": - if in.IsNull() { - in.Skip() - } else { - out.Mode = SfuMode(in.String()) - } - case "janus": - if in.IsNull() { - in.Skip() - out.Janus = nil - } else { - if out.Janus == nil { - out.Janus = new(BackendServerInfoSfuJanus) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Janus).UnmarshalEasyJSON(in) - } - } - case "proxies": - if in.IsNull() { - in.Skip() - out.Proxies = nil - } else { - in.Delim('[') - if out.Proxies == nil { - if !in.IsDelim(']') { - out.Proxies = make([]BackendServerInfoSfuProxy, 0, 0) - } else { - out.Proxies = []BackendServerInfoSfuProxy{} - } - } else { - out.Proxies = (out.Proxies)[:0] - } - for !in.IsDelim(']') { - var v7 BackendServerInfoSfuProxy - if in.IsNull() { - in.Skip() - } else { - (v7).UnmarshalEasyJSON(in) - } - out.Proxies = append(out.Proxies, v7) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk8(out *jwriter.Writer, in BackendServerInfoSfu) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"mode\":" - out.RawString(prefix[1:]) - out.String(string(in.Mode)) - } - if in.Janus != nil { - const prefix string = ",\"janus\":" - out.RawString(prefix) - (*in.Janus).MarshalEasyJSON(out) - } - if len(in.Proxies) != 0 { - const prefix string = ",\"proxies\":" - out.RawString(prefix) - { - out.RawByte('[') - for v8, v9 := range in.Proxies { - if v8 > 0 { - out.RawByte(',') - } - (v9).MarshalEasyJSON(out) - } - out.RawByte(']') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoSfu) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk8(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoSfu) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk8(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoSfu) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk8(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoSfu) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk8(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk9(in *jlexer.Lexer, out *BackendServerInfoNats) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "urls": - if in.IsNull() { - in.Skip() - out.Urls = nil - } else { - in.Delim('[') - if out.Urls == nil { - if !in.IsDelim(']') { - out.Urls = make([]string, 0, 4) - } else { - out.Urls = []string{} - } - } else { - out.Urls = (out.Urls)[:0] - } - for !in.IsDelim(']') { - var v10 string - if in.IsNull() { - in.Skip() - } else { - v10 = string(in.String()) - } - out.Urls = append(out.Urls, v10) - in.WantComma() - } - in.Delim(']') - } - case "connected": - if in.IsNull() { - in.Skip() - } else { - out.Connected = bool(in.Bool()) - } - case "serverurl": - if in.IsNull() { - in.Skip() - } else { - out.ServerUrl = string(in.String()) - } - case "serverid": - if in.IsNull() { - in.Skip() - } else { - out.ServerID = string(in.String()) - } - case "version": - if in.IsNull() { - in.Skip() - } else { - out.ServerVersion = string(in.String()) - } - case "clustername": - if in.IsNull() { - in.Skip() - } else { - out.ClusterName = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk9(out *jwriter.Writer, in BackendServerInfoNats) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"urls\":" - out.RawString(prefix[1:]) - if in.Urls == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v11, v12 := range in.Urls { - if v11 > 0 { - out.RawByte(',') - } - out.String(string(v12)) - } - out.RawByte(']') - } - } - { - const prefix string = ",\"connected\":" - out.RawString(prefix) - out.Bool(bool(in.Connected)) - } - if in.ServerUrl != "" { - const prefix string = ",\"serverurl\":" - out.RawString(prefix) - out.String(string(in.ServerUrl)) - } - if in.ServerID != "" { - const prefix string = ",\"serverid\":" - out.RawString(prefix) - out.String(string(in.ServerID)) - } - if in.ServerVersion != "" { - const prefix string = ",\"version\":" - out.RawString(prefix) - out.String(string(in.ServerVersion)) - } - if in.ClusterName != "" { - const prefix string = ",\"clustername\":" - out.RawString(prefix) - out.String(string(in.ClusterName)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoNats) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk9(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoNats) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk9(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoNats) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk9(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoNats) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk9(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk10(in *jlexer.Lexer, out *BackendServerInfoGrpc) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "target": - if in.IsNull() { - in.Skip() - } else { - out.Target = string(in.String()) - } - case "ip": - if in.IsNull() { - in.Skip() - } else { - out.IP = string(in.String()) - } - case "connected": - if in.IsNull() { - in.Skip() - } else { - out.Connected = bool(in.Bool()) - } - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk10(out *jwriter.Writer, in BackendServerInfoGrpc) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"target\":" - out.RawString(prefix[1:]) - out.String(string(in.Target)) - } - if in.IP != "" { - const prefix string = ",\"ip\":" - out.RawString(prefix) - out.String(string(in.IP)) - } - { - const prefix string = ",\"connected\":" - out.RawString(prefix) - out.Bool(bool(in.Connected)) - } - if in.Version != "" { - const prefix string = ",\"version\":" - out.RawString(prefix) - out.String(string(in.Version)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoGrpc) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk10(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoGrpc) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk10(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoGrpc) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk10(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoGrpc) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk10(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk11(in *jlexer.Lexer, out *BackendServerInfoDialout) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = api.PublicSessionId(in.String()) - } - case "connected": - if in.IsNull() { - in.Skip() - } else { - out.Connected = bool(in.Bool()) - } - case "address": - if in.IsNull() { - in.Skip() - } else { - out.Address = string(in.String()) - } - case "useragent": - if in.IsNull() { - in.Skip() - } else { - out.UserAgent = string(in.String()) - } - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "features": - if in.IsNull() { - in.Skip() - out.Features = nil - } else { - in.Delim('[') - if out.Features == nil { - if !in.IsDelim(']') { - out.Features = make([]string, 0, 4) - } else { - out.Features = []string{} - } - } else { - out.Features = (out.Features)[:0] - } - for !in.IsDelim(']') { - var v13 string - if in.IsNull() { - in.Skip() - } else { - v13 = string(in.String()) - } - out.Features = append(out.Features, v13) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk11(out *jwriter.Writer, in BackendServerInfoDialout) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"sessionid\":" - out.RawString(prefix[1:]) - out.String(string(in.SessionId)) - } - { - const prefix string = ",\"connected\":" - out.RawString(prefix) - out.Bool(bool(in.Connected)) - } - if in.Address != "" { - const prefix string = ",\"address\":" - out.RawString(prefix) - out.String(string(in.Address)) - } - if in.UserAgent != "" { - const prefix string = ",\"useragent\":" - out.RawString(prefix) - out.String(string(in.UserAgent)) - } - if in.Version != "" { - const prefix string = ",\"version\":" - out.RawString(prefix) - out.String(string(in.Version)) - } - if len(in.Features) != 0 { - const prefix string = ",\"features\":" - out.RawString(prefix) - { - out.RawByte('[') - for v14, v15 := range in.Features { - if v14 > 0 { - out.RawByte(',') - } - out.String(string(v15)) - } - out.RawByte(']') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfoDialout) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk11(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfoDialout) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk11(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfoDialout) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk11(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfoDialout) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk11(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk12(in *jlexer.Lexer, out *BackendServerInfo) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "features": - if in.IsNull() { - in.Skip() - out.Features = nil - } else { - in.Delim('[') - if out.Features == nil { - if !in.IsDelim(']') { - out.Features = make([]string, 0, 4) - } else { - out.Features = []string{} - } - } else { - out.Features = (out.Features)[:0] - } - for !in.IsDelim(']') { - var v16 string - if in.IsNull() { - in.Skip() - } else { - v16 = string(in.String()) - } - out.Features = append(out.Features, v16) - in.WantComma() - } - in.Delim(']') - } - case "sfu": - if in.IsNull() { - in.Skip() - out.Sfu = nil - } else { - if out.Sfu == nil { - out.Sfu = new(BackendServerInfoSfu) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Sfu).UnmarshalEasyJSON(in) - } - } - case "dialout": - if in.IsNull() { - in.Skip() - out.Dialout = nil - } else { - in.Delim('[') - if out.Dialout == nil { - if !in.IsDelim(']') { - out.Dialout = make([]BackendServerInfoDialout, 0, 0) - } else { - out.Dialout = []BackendServerInfoDialout{} - } - } else { - out.Dialout = (out.Dialout)[:0] - } - for !in.IsDelim(']') { - var v17 BackendServerInfoDialout - if in.IsNull() { - in.Skip() - } else { - (v17).UnmarshalEasyJSON(in) - } - out.Dialout = append(out.Dialout, v17) - in.WantComma() - } - in.Delim(']') - } - case "nats": - if in.IsNull() { - in.Skip() - out.Nats = nil - } else { - if out.Nats == nil { - out.Nats = new(BackendServerInfoNats) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Nats).UnmarshalEasyJSON(in) - } - } - case "grpc": - if in.IsNull() { - in.Skip() - out.Grpc = nil - } else { - in.Delim('[') - if out.Grpc == nil { - if !in.IsDelim(']') { - out.Grpc = make([]BackendServerInfoGrpc, 0, 1) - } else { - out.Grpc = []BackendServerInfoGrpc{} - } - } else { - out.Grpc = (out.Grpc)[:0] - } - for !in.IsDelim(']') { - var v18 BackendServerInfoGrpc - if in.IsNull() { - in.Skip() - } else { - (v18).UnmarshalEasyJSON(in) - } - out.Grpc = append(out.Grpc, v18) - in.WantComma() - } - in.Delim(']') - } - case "etcd": - if in.IsNull() { - in.Skip() - out.Etcd = nil - } else { - if out.Etcd == nil { - out.Etcd = new(etcd.BackendServerInfoEtcd) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Etcd).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk12(out *jwriter.Writer, in BackendServerInfo) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"features\":" - out.RawString(prefix) - if in.Features == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v19, v20 := range in.Features { - if v19 > 0 { - out.RawByte(',') - } - out.String(string(v20)) - } - out.RawByte(']') - } - } - if in.Sfu != nil { - const prefix string = ",\"sfu\":" - out.RawString(prefix) - (*in.Sfu).MarshalEasyJSON(out) - } - if len(in.Dialout) != 0 { - const prefix string = ",\"dialout\":" - out.RawString(prefix) - { - out.RawByte('[') - for v21, v22 := range in.Dialout { - if v21 > 0 { - out.RawByte(',') - } - (v22).MarshalEasyJSON(out) - } - out.RawByte(']') - } - } - if in.Nats != nil { - const prefix string = ",\"nats\":" - out.RawString(prefix) - (*in.Nats).MarshalEasyJSON(out) - } - if len(in.Grpc) != 0 { - const prefix string = ",\"grpc\":" - out.RawString(prefix) - { - out.RawByte('[') - for v23, v24 := range in.Grpc { - if v23 > 0 { - out.RawByte(',') - } - (v24).MarshalEasyJSON(out) - } - out.RawByte(']') - } - } - if in.Etcd != nil { - const prefix string = ",\"etcd\":" - out.RawString(prefix) - (*in.Etcd).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendServerInfo) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk12(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendServerInfo) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk12(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendServerInfo) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk12(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendServerInfo) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk12(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk13(in *jlexer.Lexer, out *BackendRoomUpdateRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "userids": - if in.IsNull() { - in.Skip() - out.UserIds = nil - } else { - in.Delim('[') - if out.UserIds == nil { - if !in.IsDelim(']') { - out.UserIds = make([]string, 0, 4) - } else { - out.UserIds = []string{} - } - } else { - out.UserIds = (out.UserIds)[:0] - } - for !in.IsDelim(']') { - var v25 string - if in.IsNull() { - in.Skip() - } else { - v25 = string(in.String()) - } - out.UserIds = append(out.UserIds, v25) - in.WantComma() - } - in.Delim(']') - } - case "properties": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Properties).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk13(out *jwriter.Writer, in BackendRoomUpdateRequest) { - out.RawByte('{') - first := true - _ = first - if len(in.UserIds) != 0 { - const prefix string = ",\"userids\":" - first = false - out.RawString(prefix[1:]) - { - out.RawByte('[') - for v26, v27 := range in.UserIds { - if v26 > 0 { - out.RawByte(',') - } - out.String(string(v27)) - } - out.RawByte(']') - } - } - if len(in.Properties) != 0 { - const prefix string = ",\"properties\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Raw((in.Properties).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomUpdateRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk13(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomUpdateRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk13(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomUpdateRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk13(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomUpdateRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk13(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk14(in *jlexer.Lexer, out *BackendRoomTransientRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "action": - if in.IsNull() { - in.Skip() - } else { - out.Action = TransientAction(in.String()) - } - case "key": - if in.IsNull() { - in.Skip() - } else { - out.Key = string(in.String()) - } - case "value": - if m, ok := out.Value.(easyjson.Unmarshaler); ok { - m.UnmarshalEasyJSON(in) - } else if m, ok := out.Value.(json.Unmarshaler); ok { - _ = m.UnmarshalJSON(in.Raw()) - } else { - out.Value = in.Interface() - } - case "ttl": - if in.IsNull() { - in.Skip() - } else { - out.TTL = time.Duration(in.Int64()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk14(out *jwriter.Writer, in BackendRoomTransientRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"action\":" - out.RawString(prefix[1:]) - out.String(string(in.Action)) - } - { - const prefix string = ",\"key\":" - out.RawString(prefix) - out.String(string(in.Key)) - } - if in.Value != nil { - const prefix string = ",\"value\":" - out.RawString(prefix) - if m, ok := in.Value.(easyjson.Marshaler); ok { - m.MarshalEasyJSON(out) - } else if m, ok := in.Value.(json.Marshaler); ok { - out.Raw(m.MarshalJSON()) - } else { - out.Raw(json.Marshal(in.Value)) - } - } - if in.TTL != 0 { - const prefix string = ",\"ttl\":" - out.RawString(prefix) - out.Int64(int64(in.TTL)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomTransientRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk14(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomTransientRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk14(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomTransientRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk14(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomTransientRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk14(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk15(in *jlexer.Lexer, out *BackendRoomSwitchToMessageRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } - case "sessions": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Sessions).UnmarshalJSON(data)) - } - } - case "sessionslist": - if in.IsNull() { - in.Skip() - out.SessionsList = nil - } else { - in.Delim('[') - if out.SessionsList == nil { - if !in.IsDelim(']') { - out.SessionsList = make(BackendRoomSwitchToPublicSessionsList, 0, 4) - } else { - out.SessionsList = BackendRoomSwitchToPublicSessionsList{} - } - } else { - out.SessionsList = (out.SessionsList)[:0] - } - for !in.IsDelim(']') { - var v28 api.PublicSessionId - if in.IsNull() { - in.Skip() - } else { - v28 = api.PublicSessionId(in.String()) - } - out.SessionsList = append(out.SessionsList, v28) - in.WantComma() - } - in.Delim(']') - } - case "sessionsmap": - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - out.SessionsMap = make(BackendRoomSwitchToPublicSessionsMap) - } else { - out.SessionsMap = nil - } - for !in.IsDelim('}') { - key := api.PublicSessionId(in.String()) - in.WantColon() - var v29 json.RawMessage - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((v29).UnmarshalJSON(data)) - } - } - (out.SessionsMap)[key] = v29 - in.WantComma() - } - in.Delim('}') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk15(out *jwriter.Writer, in BackendRoomSwitchToMessageRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"roomid\":" - out.RawString(prefix[1:]) - out.String(string(in.RoomId)) - } - if len(in.Sessions) != 0 { - const prefix string = ",\"sessions\":" - out.RawString(prefix) - out.Raw((in.Sessions).MarshalJSON()) - } - if len(in.SessionsList) != 0 { - const prefix string = ",\"sessionslist\":" - out.RawString(prefix) - { - out.RawByte('[') - for v30, v31 := range in.SessionsList { - if v30 > 0 { - out.RawByte(',') - } - out.String(string(v31)) - } - out.RawByte(']') - } - } - if len(in.SessionsMap) != 0 { - const prefix string = ",\"sessionsmap\":" - out.RawString(prefix) - { - out.RawByte('{') - v32First := true - for v32Name, v32Value := range in.SessionsMap { - if v32First { - v32First = false - } else { - out.RawByte(',') - } - out.String(string(v32Name)) - out.RawByte(':') - out.Raw((v32Value).MarshalJSON()) - } - out.RawByte('}') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomSwitchToMessageRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk15(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomSwitchToMessageRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk15(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomSwitchToMessageRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk15(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomSwitchToMessageRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk15(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk16(in *jlexer.Lexer, out *BackendRoomParticipantsRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "changed": - if in.IsNull() { - in.Skip() - out.Changed = nil - } else { - in.Delim('[') - if out.Changed == nil { - if !in.IsDelim(']') { - out.Changed = make([]api.StringMap, 0, 8) - } else { - out.Changed = []api.StringMap{} - } - } else { - out.Changed = (out.Changed)[:0] - } - for !in.IsDelim(']') { - var v33 api.StringMap - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - v33 = make(api.StringMap) - } else { - v33 = nil - } - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v34 interface{} - if m, ok := v34.(easyjson.Unmarshaler); ok { - m.UnmarshalEasyJSON(in) - } else if m, ok := v34.(json.Unmarshaler); ok { - _ = m.UnmarshalJSON(in.Raw()) - } else { - v34 = in.Interface() - } - (v33)[key] = v34 - in.WantComma() - } - in.Delim('}') - } - out.Changed = append(out.Changed, v33) - in.WantComma() - } - in.Delim(']') - } - case "users": - if in.IsNull() { - in.Skip() - out.Users = nil - } else { - in.Delim('[') - if out.Users == nil { - if !in.IsDelim(']') { - out.Users = make([]api.StringMap, 0, 8) - } else { - out.Users = []api.StringMap{} - } - } else { - out.Users = (out.Users)[:0] - } - for !in.IsDelim(']') { - var v35 api.StringMap - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - v35 = make(api.StringMap) - } else { - v35 = nil - } - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v36 interface{} - if m, ok := v36.(easyjson.Unmarshaler); ok { - m.UnmarshalEasyJSON(in) - } else if m, ok := v36.(json.Unmarshaler); ok { - _ = m.UnmarshalJSON(in.Raw()) - } else { - v36 = in.Interface() - } - (v35)[key] = v36 - in.WantComma() - } - in.Delim('}') - } - out.Users = append(out.Users, v35) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk16(out *jwriter.Writer, in BackendRoomParticipantsRequest) { - out.RawByte('{') - first := true - _ = first - if len(in.Changed) != 0 { - const prefix string = ",\"changed\":" - first = false - out.RawString(prefix[1:]) - { - out.RawByte('[') - for v37, v38 := range in.Changed { - if v37 > 0 { - out.RawByte(',') - } - if v38 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { - out.RawString(`null`) - } else { - out.RawByte('{') - v39First := true - for v39Name, v39Value := range v38 { - if v39First { - v39First = false - } else { - out.RawByte(',') - } - out.String(string(v39Name)) - out.RawByte(':') - if m, ok := v39Value.(easyjson.Marshaler); ok { - m.MarshalEasyJSON(out) - } else if m, ok := v39Value.(json.Marshaler); ok { - out.Raw(m.MarshalJSON()) - } else { - out.Raw(json.Marshal(v39Value)) - } - } - out.RawByte('}') - } - } - out.RawByte(']') - } - } - if len(in.Users) != 0 { - const prefix string = ",\"users\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - { - out.RawByte('[') - for v40, v41 := range in.Users { - if v40 > 0 { - out.RawByte(',') - } - if v41 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { - out.RawString(`null`) - } else { - out.RawByte('{') - v42First := true - for v42Name, v42Value := range v41 { - if v42First { - v42First = false - } else { - out.RawByte(',') - } - out.String(string(v42Name)) - out.RawByte(':') - if m, ok := v42Value.(easyjson.Marshaler); ok { - m.MarshalEasyJSON(out) - } else if m, ok := v42Value.(json.Marshaler); ok { - out.Raw(m.MarshalJSON()) - } else { - out.Raw(json.Marshal(v42Value)) - } - } - out.RawByte('}') - } - } - out.RawByte(']') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomParticipantsRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk16(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomParticipantsRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk16(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomParticipantsRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk16(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomParticipantsRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk16(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk17(in *jlexer.Lexer, out *BackendRoomMessageRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk17(out *jwriter.Writer, in BackendRoomMessageRequest) { - out.RawByte('{') - first := true - _ = first - if len(in.Data) != 0 { - const prefix string = ",\"data\":" - first = false - out.RawString(prefix[1:]) - out.Raw((in.Data).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomMessageRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk17(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomMessageRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk17(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomMessageRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk17(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomMessageRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk17(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk18(in *jlexer.Lexer, out *BackendRoomInviteRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "userids": - if in.IsNull() { - in.Skip() - out.UserIds = nil - } else { - in.Delim('[') - if out.UserIds == nil { - if !in.IsDelim(']') { - out.UserIds = make([]string, 0, 4) - } else { - out.UserIds = []string{} - } - } else { - out.UserIds = (out.UserIds)[:0] - } - for !in.IsDelim(']') { - var v43 string - if in.IsNull() { - in.Skip() - } else { - v43 = string(in.String()) - } - out.UserIds = append(out.UserIds, v43) - in.WantComma() - } - in.Delim(']') - } - case "alluserids": - if in.IsNull() { - in.Skip() - out.AllUserIds = nil - } else { - in.Delim('[') - if out.AllUserIds == nil { - if !in.IsDelim(']') { - out.AllUserIds = make([]string, 0, 4) - } else { - out.AllUserIds = []string{} - } - } else { - out.AllUserIds = (out.AllUserIds)[:0] - } - for !in.IsDelim(']') { - var v44 string - if in.IsNull() { - in.Skip() - } else { - v44 = string(in.String()) - } - out.AllUserIds = append(out.AllUserIds, v44) - in.WantComma() - } - in.Delim(']') - } - case "properties": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Properties).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk18(out *jwriter.Writer, in BackendRoomInviteRequest) { - out.RawByte('{') - first := true - _ = first - if len(in.UserIds) != 0 { - const prefix string = ",\"userids\":" - first = false - out.RawString(prefix[1:]) - { - out.RawByte('[') - for v45, v46 := range in.UserIds { - if v45 > 0 { - out.RawByte(',') - } - out.String(string(v46)) - } - out.RawByte(']') - } - } - if len(in.AllUserIds) != 0 { - const prefix string = ",\"alluserids\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - { - out.RawByte('[') - for v47, v48 := range in.AllUserIds { - if v47 > 0 { - out.RawByte(',') - } - out.String(string(v48)) - } - out.RawByte(']') - } - } - if len(in.Properties) != 0 { - const prefix string = ",\"properties\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Raw((in.Properties).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomInviteRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk18(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomInviteRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk18(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomInviteRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk18(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomInviteRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk18(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk19(in *jlexer.Lexer, out *BackendRoomInCallRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "incall": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.InCall).UnmarshalJSON(data)) - } - } - case "all": - if in.IsNull() { - in.Skip() - } else { - out.All = bool(in.Bool()) - } - case "changed": - if in.IsNull() { - in.Skip() - out.Changed = nil - } else { - in.Delim('[') - if out.Changed == nil { - if !in.IsDelim(']') { - out.Changed = make([]api.StringMap, 0, 8) - } else { - out.Changed = []api.StringMap{} - } - } else { - out.Changed = (out.Changed)[:0] - } - for !in.IsDelim(']') { - var v49 api.StringMap - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - v49 = make(api.StringMap) - } else { - v49 = nil - } - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v50 interface{} - if m, ok := v50.(easyjson.Unmarshaler); ok { - m.UnmarshalEasyJSON(in) - } else if m, ok := v50.(json.Unmarshaler); ok { - _ = m.UnmarshalJSON(in.Raw()) - } else { - v50 = in.Interface() - } - (v49)[key] = v50 - in.WantComma() - } - in.Delim('}') - } - out.Changed = append(out.Changed, v49) - in.WantComma() - } - in.Delim(']') - } - case "users": - if in.IsNull() { - in.Skip() - out.Users = nil - } else { - in.Delim('[') - if out.Users == nil { - if !in.IsDelim(']') { - out.Users = make([]api.StringMap, 0, 8) - } else { - out.Users = []api.StringMap{} - } - } else { - out.Users = (out.Users)[:0] - } - for !in.IsDelim(']') { - var v51 api.StringMap - if in.IsNull() { - in.Skip() - } else { - in.Delim('{') - if !in.IsDelim('}') { - v51 = make(api.StringMap) - } else { - v51 = nil - } - for !in.IsDelim('}') { - key := string(in.String()) - in.WantColon() - var v52 interface{} - if m, ok := v52.(easyjson.Unmarshaler); ok { - m.UnmarshalEasyJSON(in) - } else if m, ok := v52.(json.Unmarshaler); ok { - _ = m.UnmarshalJSON(in.Raw()) - } else { - v52 = in.Interface() - } - (v51)[key] = v52 - in.WantComma() - } - in.Delim('}') - } - out.Users = append(out.Users, v51) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk19(out *jwriter.Writer, in BackendRoomInCallRequest) { - out.RawByte('{') - first := true - _ = first - if len(in.InCall) != 0 { - const prefix string = ",\"incall\":" - first = false - out.RawString(prefix[1:]) - out.Raw((in.InCall).MarshalJSON()) - } - if in.All { - const prefix string = ",\"all\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Bool(bool(in.All)) - } - if len(in.Changed) != 0 { - const prefix string = ",\"changed\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - { - out.RawByte('[') - for v53, v54 := range in.Changed { - if v53 > 0 { - out.RawByte(',') - } - if v54 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { - out.RawString(`null`) - } else { - out.RawByte('{') - v55First := true - for v55Name, v55Value := range v54 { - if v55First { - v55First = false - } else { - out.RawByte(',') - } - out.String(string(v55Name)) - out.RawByte(':') - if m, ok := v55Value.(easyjson.Marshaler); ok { - m.MarshalEasyJSON(out) - } else if m, ok := v55Value.(json.Marshaler); ok { - out.Raw(m.MarshalJSON()) - } else { - out.Raw(json.Marshal(v55Value)) - } - } - out.RawByte('}') - } - } - out.RawByte(']') - } - } - if len(in.Users) != 0 { - const prefix string = ",\"users\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - { - out.RawByte('[') - for v56, v57 := range in.Users { - if v56 > 0 { - out.RawByte(',') - } - if v57 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { - out.RawString(`null`) - } else { - out.RawByte('{') - v58First := true - for v58Name, v58Value := range v57 { - if v58First { - v58First = false - } else { - out.RawByte(',') - } - out.String(string(v58Name)) - out.RawByte(':') - if m, ok := v58Value.(easyjson.Marshaler); ok { - m.MarshalEasyJSON(out) - } else if m, ok := v58Value.(json.Marshaler); ok { - out.Raw(m.MarshalJSON()) - } else { - out.Raw(json.Marshal(v58Value)) - } - } - out.RawByte('}') - } - } - out.RawByte(']') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomInCallRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk19(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomInCallRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk19(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomInCallRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk19(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomInCallRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk19(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk20(in *jlexer.Lexer, out *BackendRoomDisinviteRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "userids": - if in.IsNull() { - in.Skip() - out.UserIds = nil - } else { - in.Delim('[') - if out.UserIds == nil { - if !in.IsDelim(']') { - out.UserIds = make([]string, 0, 4) - } else { - out.UserIds = []string{} - } - } else { - out.UserIds = (out.UserIds)[:0] - } - for !in.IsDelim(']') { - var v59 string - if in.IsNull() { - in.Skip() - } else { - v59 = string(in.String()) - } - out.UserIds = append(out.UserIds, v59) - in.WantComma() - } - in.Delim(']') - } - case "sessionids": - if in.IsNull() { - in.Skip() - out.SessionIds = nil - } else { - in.Delim('[') - if out.SessionIds == nil { - if !in.IsDelim(']') { - out.SessionIds = make([]api.RoomSessionId, 0, 4) - } else { - out.SessionIds = []api.RoomSessionId{} - } - } else { - out.SessionIds = (out.SessionIds)[:0] - } - for !in.IsDelim(']') { - var v60 api.RoomSessionId - if in.IsNull() { - in.Skip() - } else { - v60 = api.RoomSessionId(in.String()) - } - out.SessionIds = append(out.SessionIds, v60) - in.WantComma() - } - in.Delim(']') - } - case "alluserids": - if in.IsNull() { - in.Skip() - out.AllUserIds = nil - } else { - in.Delim('[') - if out.AllUserIds == nil { - if !in.IsDelim(']') { - out.AllUserIds = make([]string, 0, 4) - } else { - out.AllUserIds = []string{} - } - } else { - out.AllUserIds = (out.AllUserIds)[:0] - } - for !in.IsDelim(']') { - var v61 string - if in.IsNull() { - in.Skip() - } else { - v61 = string(in.String()) - } - out.AllUserIds = append(out.AllUserIds, v61) - in.WantComma() - } - in.Delim(']') - } - case "properties": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Properties).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk20(out *jwriter.Writer, in BackendRoomDisinviteRequest) { - out.RawByte('{') - first := true - _ = first - if len(in.UserIds) != 0 { - const prefix string = ",\"userids\":" - first = false - out.RawString(prefix[1:]) - { - out.RawByte('[') - for v62, v63 := range in.UserIds { - if v62 > 0 { - out.RawByte(',') - } - out.String(string(v63)) - } - out.RawByte(']') - } - } - if len(in.SessionIds) != 0 { - const prefix string = ",\"sessionids\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - { - out.RawByte('[') - for v64, v65 := range in.SessionIds { - if v64 > 0 { - out.RawByte(',') - } - out.String(string(v65)) - } - out.RawByte(']') - } - } - if len(in.AllUserIds) != 0 { - const prefix string = ",\"alluserids\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - { - out.RawByte('[') - for v66, v67 := range in.AllUserIds { - if v66 > 0 { - out.RawByte(',') - } - out.String(string(v67)) - } - out.RawByte(']') - } - } - if len(in.Properties) != 0 { - const prefix string = ",\"properties\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.Raw((in.Properties).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomDisinviteRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk20(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomDisinviteRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk20(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomDisinviteRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk20(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomDisinviteRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk20(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk21(in *jlexer.Lexer, out *BackendRoomDialoutResponse) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "callid": - if in.IsNull() { - in.Skip() - } else { - out.CallId = string(in.String()) - } - case "error": - if in.IsNull() { - in.Skip() - out.Error = nil - } else { - if out.Error == nil { - out.Error = new(api.Error) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Error).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk21(out *jwriter.Writer, in BackendRoomDialoutResponse) { - out.RawByte('{') - first := true - _ = first - if in.CallId != "" { - const prefix string = ",\"callid\":" - first = false - out.RawString(prefix[1:]) - out.String(string(in.CallId)) - } - if in.Error != nil { - const prefix string = ",\"error\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - (*in.Error).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomDialoutResponse) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk21(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomDialoutResponse) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk21(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomDialoutResponse) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk21(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomDialoutResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk21(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk22(in *jlexer.Lexer, out *BackendRoomDialoutRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "number": - if in.IsNull() { - in.Skip() - } else { - out.Number = string(in.String()) - } - case "options": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Options).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk22(out *jwriter.Writer, in BackendRoomDialoutRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"number\":" - out.RawString(prefix[1:]) - out.String(string(in.Number)) - } - if len(in.Options) != 0 { - const prefix string = ",\"options\":" - out.RawString(prefix) - out.Raw((in.Options).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomDialoutRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk22(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomDialoutRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk22(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomDialoutRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk22(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomDialoutRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk22(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk23(in *jlexer.Lexer, out *BackendRoomDialoutError) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "code": - if in.IsNull() { - in.Skip() - } else { - out.Code = string(in.String()) - } - case "message": - if in.IsNull() { - in.Skip() - } else { - out.Message = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk23(out *jwriter.Writer, in BackendRoomDialoutError) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"code\":" - out.RawString(prefix[1:]) - out.String(string(in.Code)) - } - if in.Message != "" { - const prefix string = ",\"message\":" - out.RawString(prefix) - out.String(string(in.Message)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomDialoutError) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk23(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomDialoutError) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk23(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomDialoutError) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk23(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomDialoutError) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk23(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk24(in *jlexer.Lexer, out *BackendRoomDeleteRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "userids": - if in.IsNull() { - in.Skip() - out.UserIds = nil - } else { - in.Delim('[') - if out.UserIds == nil { - if !in.IsDelim(']') { - out.UserIds = make([]string, 0, 4) - } else { - out.UserIds = []string{} - } - } else { - out.UserIds = (out.UserIds)[:0] - } - for !in.IsDelim(']') { - var v68 string - if in.IsNull() { - in.Skip() - } else { - v68 = string(in.String()) - } - out.UserIds = append(out.UserIds, v68) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk24(out *jwriter.Writer, in BackendRoomDeleteRequest) { - out.RawByte('{') - first := true - _ = first - if len(in.UserIds) != 0 { - const prefix string = ",\"userids\":" - first = false - out.RawString(prefix[1:]) - { - out.RawByte('[') - for v69, v70 := range in.UserIds { - if v69 > 0 { - out.RawByte(',') - } - out.String(string(v70)) - } - out.RawByte(']') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendRoomDeleteRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk24(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendRoomDeleteRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk24(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendRoomDeleteRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk24(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendRoomDeleteRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk24(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk25(in *jlexer.Lexer, out *BackendPingEntry) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } - case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = api.RoomSessionId(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk25(out *jwriter.Writer, in BackendPingEntry) { - out.RawByte('{') - first := true - _ = first - if in.UserId != "" { - const prefix string = ",\"userid\":" - first = false - out.RawString(prefix[1:]) - out.String(string(in.UserId)) - } - { - const prefix string = ",\"sessionid\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.SessionId)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendPingEntry) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk25(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendPingEntry) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk25(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendPingEntry) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk25(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendPingEntry) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk25(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk26(in *jlexer.Lexer, out *BackendClientSessionResponse) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk26(out *jwriter.Writer, in BackendClientSessionResponse) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"roomid\":" - out.RawString(prefix) - out.String(string(in.RoomId)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientSessionResponse) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk26(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientSessionResponse) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk26(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientSessionResponse) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk26(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientSessionResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk26(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk27(in *jlexer.Lexer, out *BackendClientSessionRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } - case "action": - if in.IsNull() { - in.Skip() - } else { - out.Action = string(in.String()) - } - case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = api.PublicSessionId(in.String()) - } - case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } - case "user": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.User).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk27(out *jwriter.Writer, in BackendClientSessionRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"roomid\":" - out.RawString(prefix) - out.String(string(in.RoomId)) - } - { - const prefix string = ",\"action\":" - out.RawString(prefix) - out.String(string(in.Action)) - } - { - const prefix string = ",\"sessionid\":" - out.RawString(prefix) - out.String(string(in.SessionId)) - } - if in.UserId != "" { - const prefix string = ",\"userid\":" - out.RawString(prefix) - out.String(string(in.UserId)) - } - if len(in.User) != 0 { - const prefix string = ",\"user\":" - out.RawString(prefix) - out.Raw((in.User).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientSessionRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk27(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientSessionRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk27(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientSessionRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk27(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientSessionRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk27(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk28(in *jlexer.Lexer, out *BackendClientRoomResponse) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } - case "properties": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Properties).UnmarshalJSON(data)) - } - } - case "session": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Session).UnmarshalJSON(data)) - } - } - case "permissions": - if in.IsNull() { - in.Skip() - out.Permissions = nil - } else { - if out.Permissions == nil { - out.Permissions = new([]api.Permission) - } - if in.IsNull() { - in.Skip() - *out.Permissions = nil - } else { - in.Delim('[') - if *out.Permissions == nil { - if !in.IsDelim(']') { - *out.Permissions = make([]api.Permission, 0, 4) - } else { - *out.Permissions = []api.Permission{} - } - } else { - *out.Permissions = (*out.Permissions)[:0] - } - for !in.IsDelim(']') { - var v71 api.Permission - if in.IsNull() { - in.Skip() - } else { - v71 = api.Permission(in.String()) - } - *out.Permissions = append(*out.Permissions, v71) - in.WantComma() - } - in.Delim(']') - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk28(out *jwriter.Writer, in BackendClientRoomResponse) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"roomid\":" - out.RawString(prefix) - out.String(string(in.RoomId)) - } - { - const prefix string = ",\"properties\":" - out.RawString(prefix) - out.Raw((in.Properties).MarshalJSON()) - } - if len(in.Session) != 0 { - const prefix string = ",\"session\":" - out.RawString(prefix) - out.Raw((in.Session).MarshalJSON()) - } - if in.Permissions != nil { - const prefix string = ",\"permissions\":" - out.RawString(prefix) - if *in.Permissions == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v72, v73 := range *in.Permissions { - if v72 > 0 { - out.RawByte(',') - } - out.String(string(v73)) - } - out.RawByte(']') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientRoomResponse) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk28(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientRoomResponse) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk28(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientRoomResponse) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk28(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientRoomResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk28(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk29(in *jlexer.Lexer, out *BackendClientRoomRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } - case "action": - if in.IsNull() { - in.Skip() - } else { - out.Action = string(in.String()) - } - case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } - case "sessionid": - if in.IsNull() { - in.Skip() - } else { - out.SessionId = api.RoomSessionId(in.String()) - } - case "actorid": - if in.IsNull() { - in.Skip() - } else { - out.ActorId = string(in.String()) - } - case "actortype": - if in.IsNull() { - in.Skip() - } else { - out.ActorType = string(in.String()) - } - case "incall": - if in.IsNull() { - in.Skip() - } else { - out.InCall = int(in.Int()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk29(out *jwriter.Writer, in BackendClientRoomRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"roomid\":" - out.RawString(prefix) - out.String(string(in.RoomId)) - } - if in.Action != "" { - const prefix string = ",\"action\":" - out.RawString(prefix) - out.String(string(in.Action)) - } - { - const prefix string = ",\"userid\":" - out.RawString(prefix) - out.String(string(in.UserId)) - } - { - const prefix string = ",\"sessionid\":" - out.RawString(prefix) - out.String(string(in.SessionId)) - } - if in.ActorId != "" { - const prefix string = ",\"actorid\":" - out.RawString(prefix) - out.String(string(in.ActorId)) - } - if in.ActorType != "" { - const prefix string = ",\"actortype\":" - out.RawString(prefix) - out.String(string(in.ActorType)) - } - if in.InCall != 0 { - const prefix string = ",\"incall\":" - out.RawString(prefix) - out.Int(int(in.InCall)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientRoomRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk29(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientRoomRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk29(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientRoomRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk29(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientRoomRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk29(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk30(in *jlexer.Lexer, out *BackendClientRingResponse) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk30(out *jwriter.Writer, in BackendClientRingResponse) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"roomid\":" - out.RawString(prefix) - out.String(string(in.RoomId)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientRingResponse) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk30(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientRingResponse) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk30(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientRingResponse) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk30(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientRingResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk30(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk31(in *jlexer.Lexer, out *BackendClientResponse) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } - case "error": - if in.IsNull() { - in.Skip() - out.Error = nil - } else { - if out.Error == nil { - out.Error = new(api.Error) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Error).UnmarshalEasyJSON(in) - } - } - case "auth": - if in.IsNull() { - in.Skip() - out.Auth = nil - } else { - if out.Auth == nil { - out.Auth = new(BackendClientAuthResponse) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Auth).UnmarshalEasyJSON(in) - } - } - case "room": - if in.IsNull() { - in.Skip() - out.Room = nil - } else { - if out.Room == nil { - out.Room = new(BackendClientRoomResponse) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Room).UnmarshalEasyJSON(in) - } - } - case "ping": - if in.IsNull() { - in.Skip() - out.Ping = nil - } else { - if out.Ping == nil { - out.Ping = new(BackendClientRingResponse) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Ping).UnmarshalEasyJSON(in) - } - } - case "session": - if in.IsNull() { - in.Skip() - out.Session = nil - } else { - if out.Session == nil { - out.Session = new(BackendClientSessionResponse) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Session).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk31(out *jwriter.Writer, in BackendClientResponse) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"type\":" - out.RawString(prefix[1:]) - out.String(string(in.Type)) - } - if in.Error != nil { - const prefix string = ",\"error\":" - out.RawString(prefix) - (*in.Error).MarshalEasyJSON(out) - } - if in.Auth != nil { - const prefix string = ",\"auth\":" - out.RawString(prefix) - (*in.Auth).MarshalEasyJSON(out) - } - if in.Room != nil { - const prefix string = ",\"room\":" - out.RawString(prefix) - (*in.Room).MarshalEasyJSON(out) - } - if in.Ping != nil { - const prefix string = ",\"ping\":" - out.RawString(prefix) - (*in.Ping).MarshalEasyJSON(out) - } - if in.Session != nil { - const prefix string = ",\"session\":" - out.RawString(prefix) - (*in.Session).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientResponse) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk31(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientResponse) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk31(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientResponse) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk31(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk31(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk32(in *jlexer.Lexer, out *BackendClientRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "type": - if in.IsNull() { - in.Skip() - } else { - out.Type = string(in.String()) - } - case "auth": - if in.IsNull() { - in.Skip() - out.Auth = nil - } else { - if out.Auth == nil { - out.Auth = new(BackendClientAuthRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Auth).UnmarshalEasyJSON(in) - } - } - case "room": - if in.IsNull() { - in.Skip() - out.Room = nil - } else { - if out.Room == nil { - out.Room = new(BackendClientRoomRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Room).UnmarshalEasyJSON(in) - } - } - case "ping": - if in.IsNull() { - in.Skip() - out.Ping = nil - } else { - if out.Ping == nil { - out.Ping = new(BackendClientPingRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Ping).UnmarshalEasyJSON(in) - } - } - case "session": - if in.IsNull() { - in.Skip() - out.Session = nil - } else { - if out.Session == nil { - out.Session = new(BackendClientSessionRequest) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Session).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk32(out *jwriter.Writer, in BackendClientRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"type\":" - out.RawString(prefix[1:]) - out.String(string(in.Type)) - } - if in.Auth != nil { - const prefix string = ",\"auth\":" - out.RawString(prefix) - (*in.Auth).MarshalEasyJSON(out) - } - if in.Room != nil { - const prefix string = ",\"room\":" - out.RawString(prefix) - (*in.Room).MarshalEasyJSON(out) - } - if in.Ping != nil { - const prefix string = ",\"ping\":" - out.RawString(prefix) - (*in.Ping).MarshalEasyJSON(out) - } - if in.Session != nil { - const prefix string = ",\"session\":" - out.RawString(prefix) - (*in.Session).MarshalEasyJSON(out) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk32(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk32(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk32(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk32(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk33(in *jlexer.Lexer, out *BackendClientPingRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "roomid": - if in.IsNull() { - in.Skip() - } else { - out.RoomId = string(in.String()) - } - case "entries": - if in.IsNull() { - in.Skip() - out.Entries = nil - } else { - in.Delim('[') - if out.Entries == nil { - if !in.IsDelim(']') { - out.Entries = make([]BackendPingEntry, 0, 2) - } else { - out.Entries = []BackendPingEntry{} - } - } else { - out.Entries = (out.Entries)[:0] - } - for !in.IsDelim(']') { - var v74 BackendPingEntry - if in.IsNull() { - in.Skip() - } else { - (v74).UnmarshalEasyJSON(in) - } - out.Entries = append(out.Entries, v74) - in.WantComma() - } - in.Delim(']') - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk33(out *jwriter.Writer, in BackendClientPingRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"roomid\":" - out.RawString(prefix) - out.String(string(in.RoomId)) - } - { - const prefix string = ",\"entries\":" - out.RawString(prefix) - if in.Entries == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v75, v76 := range in.Entries { - if v75 > 0 { - out.RawByte(',') - } - (v76).MarshalEasyJSON(out) - } - out.RawByte(']') - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientPingRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk33(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientPingRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk33(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientPingRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk33(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientPingRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk33(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk34(in *jlexer.Lexer, out *BackendClientAuthResponse) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "userid": - if in.IsNull() { - in.Skip() - } else { - out.UserId = string(in.String()) - } - case "user": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.User).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk34(out *jwriter.Writer, in BackendClientAuthResponse) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"userid\":" - out.RawString(prefix) - out.String(string(in.UserId)) - } - { - const prefix string = ",\"user\":" - out.RawString(prefix) - out.Raw((in.User).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientAuthResponse) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk34(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientAuthResponse) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk34(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientAuthResponse) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk34(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientAuthResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk34(l, v) -} -func easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk35(in *jlexer.Lexer, out *BackendClientAuthRequest) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "version": - if in.IsNull() { - in.Skip() - } else { - out.Version = string(in.String()) - } - case "params": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Params).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk35(out *jwriter.Writer, in BackendClientAuthRequest) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"version\":" - out.RawString(prefix[1:]) - out.String(string(in.Version)) - } - { - const prefix string = ",\"params\":" - out.RawString(prefix) - out.Raw((in.Params).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v BackendClientAuthRequest) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk35(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v BackendClientAuthRequest) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC1cedd36EncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk35(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *BackendClientAuthRequest) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk35(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *BackendClientAuthRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC1cedd36DecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk35(l, v) -} diff --git a/talk/backend.go b/talk/backend.go deleted file mode 100644 index c9ef31c..0000000 --- a/talk/backend.go +++ /dev/null @@ -1,314 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2020 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package talk - -import ( - "bytes" - "fmt" - "net/url" - "slices" - "strings" - "sync" - - "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" -) - -var ( - SessionLimitExceeded = api.NewError("session_limit_exceeded", "Too many sessions connected for this backend.") // +checklocksignore: Global readonly variable. -) - -func init() { - registerBackendStats() -} - -type Backend struct { - id string - urls []string - secret []byte - - allowHttp bool - - maxStreamBitrate api.Bandwidth - maxScreenBitrate api.Bandwidth - - sessionLimit uint64 - sessionsLock sync.Mutex - // +checklocks:sessionsLock - sessions map[api.PublicSessionId]bool - - counted bool -} - -func NewCompatBackend(cfg *goconf.ConfigFile) *Backend { - if cfg == nil { - return &Backend{ - id: "compat", - } - } - - allowHttp, _ := cfg.GetBool("backend", "allowhttp") - commonSecret, _ := config.GetStringOptionWithEnv(cfg, "backend", "secret") - sessionLimit, err := cfg.GetInt("backend", "sessionlimit") - if err != nil || sessionLimit < 0 { - sessionLimit = 0 - } - maxStreamBitrate, err := cfg.GetInt("backend", "maxstreambitrate") - if err != nil || maxStreamBitrate < 0 { - maxStreamBitrate = 0 - } - maxScreenBitrate, err := cfg.GetInt("backend", "maxscreenbitrate") - if err != nil || maxScreenBitrate < 0 { - maxScreenBitrate = 0 - } - - return &Backend{ - id: "compat", - secret: []byte(commonSecret), - - allowHttp: allowHttp, - - sessionLimit: uint64(sessionLimit), - counted: true, - - maxStreamBitrate: api.BandwidthFromBits(uint64(maxStreamBitrate)), - maxScreenBitrate: api.BandwidthFromBits(uint64(maxScreenBitrate)), - } -} - -func NewBackendFromConfig(logger log.Logger, id string, cfg *goconf.ConfigFile, commonSecret string) (*Backend, error) { - secret, _ := config.GetStringOptionWithEnv(cfg, id, "secret") - if secret == "" && commonSecret != "" { - logger.Printf("Backend %s has no own shared secret set, using common shared secret", id) - secret = commonSecret - } - if secret == "" { - return nil, fmt.Errorf("backend %s is missing or incomplete, skipping", id) - } - - sessionLimit, err := cfg.GetInt(id, "sessionlimit") - if err != nil || sessionLimit < 0 { - sessionLimit = 0 - } - maxStreamBitrate, err := cfg.GetInt(id, "maxstreambitrate") - if err != nil || maxStreamBitrate < 0 { - maxStreamBitrate = 0 - } - maxScreenBitrate, err := cfg.GetInt(id, "maxscreenbitrate") - if err != nil || maxScreenBitrate < 0 { - maxScreenBitrate = 0 - } - - return &Backend{ - id: id, - secret: []byte(secret), - - maxStreamBitrate: api.BandwidthFromBits(uint64(maxStreamBitrate)), - maxScreenBitrate: api.BandwidthFromBits(uint64(maxScreenBitrate)), - - sessionLimit: uint64(sessionLimit), - }, nil -} - -func NewBackendFromEtcd(key string, info *etcd.BackendInformationEtcd) *Backend { - allowHttp := slices.ContainsFunc(info.ParsedUrls, func(u *url.URL) bool { - return u.Scheme == "http" - }) - - return &Backend{ - id: key, - urls: info.Urls, - secret: []byte(info.Secret), - - allowHttp: allowHttp, - - maxStreamBitrate: info.MaxStreamBitrate, - maxScreenBitrate: info.MaxScreenBitrate, - sessionLimit: info.SessionLimit, - } -} - -func (b *Backend) Id() string { - return b.id -} - -func (b *Backend) Secret() []byte { - return b.secret -} - -func (b *Backend) IsCompat() bool { - return len(b.urls) == 0 -} - -func (b *Backend) Equal(other *Backend) bool { - if b == other { - return true - } else if b == nil || other == nil { - return false - } - - return b.id == other.id && - b.allowHttp == other.allowHttp && - b.maxStreamBitrate == other.maxStreamBitrate && - b.maxScreenBitrate == other.maxScreenBitrate && - b.sessionLimit == other.sessionLimit && - bytes.Equal(b.secret, other.secret) && - slices.Equal(b.urls, other.urls) -} - -func (b *Backend) IsUrlAllowed(u *url.URL) bool { - switch u.Scheme { - case "https": - return true - case "http": - return b.allowHttp - default: - return false - } -} - -func (b *Backend) HasUrl(url string) bool { - if b.IsCompat() { - // Old-style configuration, only hosts are configured. - return true - } - - for _, u := range b.urls { - if strings.HasPrefix(url, u) { - return true - } - } - - return false -} - -func (b *Backend) Urls() []string { - return b.urls -} - -func (b *Backend) AddUrl(u *url.URL) { - b.urls = append(b.urls, u.String()) - if u.Scheme == "http" { - b.allowHttp = true - } -} - -func (b *Backend) Limit() int { - return int(b.sessionLimit) -} - -func (b *Backend) SetMaxStreamBitrate(bitrate api.Bandwidth) { - b.maxStreamBitrate = bitrate -} - -func (b *Backend) MaxStreamBitrate() api.Bandwidth { - return b.maxStreamBitrate -} - -func (b *Backend) SetMaxScreenBitrate(bitrate api.Bandwidth) { - b.maxScreenBitrate = bitrate -} - -func (b *Backend) MaxScreenBitrate() api.Bandwidth { - return b.maxScreenBitrate -} - -func (b *Backend) CopyCount(other *Backend) { - b.counted = other.counted -} - -func (b *Backend) Count() bool { - if b.counted { - return false - } - - b.counted = true - return true -} - -func (b *Backend) Uncount() bool { - if !b.counted { - return false - } - - b.counted = false - return true -} - -func (b *Backend) Len() int { - b.sessionsLock.Lock() - defer b.sessionsLock.Unlock() - return len(b.sessions) -} - -type BackendSession interface { - PublicId() api.PublicSessionId - ClientType() api.ClientType -} - -func (b *Backend) AddSession(session BackendSession) error { - if session.ClientType() == api.HelloClientTypeInternal || session.ClientType() == api.HelloClientTypeVirtual { - // Internal and virtual sessions are not counting to the limit. - return nil - } - - if b.sessionLimit == 0 { - // Not limited - return nil - } - - b.sessionsLock.Lock() - defer b.sessionsLock.Unlock() - if b.sessions == nil { - b.sessions = make(map[api.PublicSessionId]bool) - } else if uint64(len(b.sessions)) >= b.sessionLimit { - registerBackendStats() - statsBackendLimitExceededTotal.WithLabelValues(b.id).Inc() - return SessionLimitExceeded - } - - b.sessions[session.PublicId()] = true - return nil -} - -func (b *Backend) RemoveSession(session BackendSession) { - b.sessionsLock.Lock() - defer b.sessionsLock.Unlock() - - delete(b.sessions, session.PublicId()) -} - -func (b *Backend) UpdateStats() { - if b.sessionLimit > 0 { - statsBackendLimit.WithLabelValues(b.id).Set(float64(b.sessionLimit)) - } else { - statsBackendLimit.DeleteLabelValues(b.id) - } -} - -func (b *Backend) DeleteStats() { - statsBackendLimit.DeleteLabelValues(b.id) -} diff --git a/talk/backend_client.go b/talk/backend_client.go deleted file mode 100644 index 16b9614..0000000 --- a/talk/backend_client.go +++ /dev/null @@ -1,276 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2017 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package talk - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/etcd" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/pool" -) - -var ( - ErrUnsupportedContentType = errors.New("unsupported_content_type") - - ErrIncompleteResponse = errors.New("incomplete OCS response") - ErrThrottledResponse = errors.New("throttled OCS response") -) - -func init() { - RegisterBackendClientStats() -} - -type FeaturesFunc = func() []string - -var ( - emptyFeaturesFunc = func() []string { - return nil - } -) - -type BackendClient struct { - version string - features FeaturesFunc - backends *BackendConfiguration - - pool *pool.HttpClientPool - capabilities *Capabilities - buffers pool.BufferPool -} - -func NewBackendClient(ctx context.Context, config *goconf.ConfigFile, maxConcurrentRequestsPerHost int, version string, etcdClient etcd.Client) (*BackendClient, error) { - logger := log.LoggerFromContext(ctx) - backends, err := NewBackendConfiguration(logger, config, etcdClient) - if err != nil { - return nil, err - } - - skipverify, _ := config.GetBool("backend", "skipverify") - if skipverify { - logger.Println("WARNING: Backend verification is disabled!") - } - - pool, err := pool.NewHttpClientPool(maxConcurrentRequestsPerHost, skipverify) - if err != nil { - return nil, err - } - - capabilities, err := NewCapabilities(version, pool) - if err != nil { - return nil, err - } - - return &BackendClient{ - version: version, - features: emptyFeaturesFunc, - backends: backends, - - pool: pool, - capabilities: capabilities, - }, nil -} - -func (b *BackendClient) Close() { - b.backends.Close() -} - -func (b *BackendClient) Reload(config *goconf.ConfigFile) { - b.backends.Reload(config) -} - -func (b *BackendClient) GetCompatBackend() *Backend { - return b.backends.GetCompatBackend() -} - -func (b *BackendClient) GetBackend(u *url.URL) *Backend { - return b.backends.GetBackend(u) -} - -func (b *BackendClient) GetBackends() []*Backend { - return b.backends.GetBackends() -} - -func (b *BackendClient) IsUrlAllowed(u *url.URL) bool { - return b.backends.IsUrlAllowed(u) -} - -func (b *BackendClient) HasCapabilityFeature(ctx context.Context, u *url.URL, feature string) bool { - return b.capabilities.HasCapabilityFeature(ctx, u, feature) -} - -func (b *BackendClient) GetStringConfig(ctx context.Context, u *url.URL, group, key string) (string, bool, bool) { - return b.capabilities.GetStringConfig(ctx, u, group, key) -} - -func (b *BackendClient) GetIntegerConfig(ctx context.Context, u *url.URL, group, key string) (int, bool, bool) { - return b.capabilities.GetIntegerConfig(ctx, u, group, key) -} - -func (b *BackendClient) InvalidateCapabilities(u *url.URL) { - b.capabilities.InvalidateCapabilities(u) -} - -func (b *BackendClient) SetFeaturesFunc(f func() []string) { - if f == nil { - f = emptyFeaturesFunc - } - - b.features = f -} - -// PerformJSONRequest sends a JSON POST request to the given url and decodes -// the result into "response". -func (b *BackendClient) PerformJSONRequest(ctx context.Context, u *url.URL, request any, response any) error { - logger := log.LoggerFromContext(ctx) - if u == nil { - return fmt.Errorf("no url passed to perform JSON request %+v", request) - } - - backend := b.backends.GetBackend(u) - if backend == nil { - return fmt.Errorf("no backend configured for %s", u) - } - - var requestUrl *url.URL - if b.capabilities.HasCapabilityFeature(ctx, u, FeatureSignalingV3Api) { - newUrl := *u - newUrl.Path = strings.ReplaceAll(newUrl.Path, "/spreed/api/v1/signaling/", "/spreed/api/v3/signaling/") - newUrl.Path = strings.ReplaceAll(newUrl.Path, "/spreed/api/v2/signaling/", "/spreed/api/v3/signaling/") - requestUrl = &newUrl - } else { - requestUrl = u - } - - c, pool, err := b.pool.Get(ctx, u) - if err != nil { - logger.Printf("Could not get client for host %s: %s", u.Host, err) - return err - } - defer pool.Put(c) - - data, err := b.buffers.MarshalAsJSON(request) - if err != nil { - logger.Printf("Could not marshal request %+v: %s", request, err) - return err - } - - defer b.buffers.Put(data) - req, err := http.NewRequestWithContext(ctx, "POST", requestUrl.String(), data) - if err != nil { - logger.Printf("Could not create request to %s: %s", requestUrl, err) - return err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - req.Header.Set("OCS-APIRequest", "true") - req.Header.Set("User-Agent", "nextcloud-spreed-signaling/"+b.version) - if features := b.features(); len(features) > 0 { - req.Header.Set("X-Spreed-Signaling-Features", strings.Join(features, ", ")) - } - - // Add checksum so the backend can validate the request. - AddBackendChecksum(req, data.Bytes(), backend.Secret()) - - start := time.Now() - resp, err := c.Do(req) - end := time.Now() - duration := end.Sub(start) - statsBackendClientRequests.WithLabelValues(backend.Id()).Inc() - statsBackendClientDuration.WithLabelValues(backend.Id()).Observe(duration.Seconds()) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - statsBackendClientError.WithLabelValues(backend.Id(), "timeout").Inc() - } else if errors.Is(err, context.Canceled) { - statsBackendClientError.WithLabelValues(backend.Id(), "canceled").Inc() - } else { - statsBackendClientError.WithLabelValues(backend.Id(), "unknown").Inc() - } - logger.Printf("Could not send request %s to %s: %s", data.String(), req.URL, err) - return err - } - defer resp.Body.Close() - - ct := resp.Header.Get("Content-Type") - if !strings.HasPrefix(ct, "application/json") { - logger.Printf("Received unsupported content-type from %s for %s: %s (%s)", req.URL, data.String(), ct, resp.Status) - statsBackendClientError.WithLabelValues(backend.Id(), "invalid_content_type").Inc() - return ErrUnsupportedContentType - } - - body, err := b.buffers.ReadAll(resp.Body) - if err != nil { - logger.Printf("Could not read response body from %s for %s: %s", req.URL, data.String(), err) - statsBackendClientError.WithLabelValues(backend.Id(), "error_reading_body").Inc() - return err - } - - defer b.buffers.Put(body) - - if IsOcsRequest(u) || req.Header.Get("OCS-APIRequest") != "" { - // OCS response are wrapped in an OCS container that needs to be parsed - // to get the actual contents: - // { - // "ocs": { - // "meta": { ... }, - // "data": { ... } - // } - // } - var ocs OcsResponse - if err := json.Unmarshal(body.Bytes(), &ocs); err != nil { - logger.Printf("Could not decode OCS response %s from %s: %s", body.String(), req.URL, err) - statsBackendClientError.WithLabelValues(backend.Id(), "error_decoding_ocs").Inc() - return err - } else if ocs.Ocs == nil || len(ocs.Ocs.Data) == 0 { - logger.Printf("Incomplete OCS response %s from %s", body.String(), req.URL) - statsBackendClientError.WithLabelValues(backend.Id(), "error_incomplete_ocs").Inc() - return ErrIncompleteResponse - } - - switch ocs.Ocs.Meta.StatusCode { - case http.StatusTooManyRequests: - logger.Printf("Throttled OCS response %s from %s", body.String(), req.URL) - statsBackendClientError.WithLabelValues(backend.Id(), "throttled").Inc() - return ErrThrottledResponse - } - - if err := json.Unmarshal(ocs.Ocs.Data, response); err != nil { - logger.Printf("Could not decode OCS response body %s from %s: %s", string(ocs.Ocs.Data), req.URL, err) - statsBackendClientError.WithLabelValues(backend.Id(), "error_decoding_ocs_data").Inc() - return err - } - } else if err := json.Unmarshal(body.Bytes(), response); err != nil { - logger.Printf("Could not decode response body %s from %s: %s", body.String(), req.URL, err) - statsBackendClientError.WithLabelValues(backend.Id(), "error_decoding_body").Inc() - return err - } - return nil -} diff --git a/talk/backend_client_stats_prometheus.go b/talk/backend_client_stats_prometheus.go deleted file mode 100644 index 8c07ee5..0000000 --- a/talk/backend_client_stats_prometheus.go +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package talk - -import ( - "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" -) - -var ( - statsBackendClientRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "backend_client", - Name: "requests_total", - Help: "The total number of backend client requests", - }, []string{"backend"}) - statsBackendClientDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "signaling", - Subsystem: "backend_client", - Name: "requests_duration", - Help: "The duration of backend client requests in seconds", - Buckets: prometheus.ExponentialBucketsRange(0.01, 30, 30), - }, []string{"backend"}) - statsBackendClientError = prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "signaling", - Subsystem: "backend_client", - Name: "requests_errors_total", - Help: "The total number of backend client requests that had an error", - }, []string{"backend", "error"}) - - backendClientStats = []prometheus.Collector{ - statsBackendClientRequests, - statsBackendClientDuration, - statsBackendClientError, - } -) - -func RegisterBackendClientStats() { - metrics.RegisterAll(backendClientStats...) -} diff --git a/talk/backend_storage_static.go b/talk/backend_storage_static.go deleted file mode 100644 index b1ced2b..0000000 --- a/talk/backend_storage_static.go +++ /dev/null @@ -1,372 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2022 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package talk - -import ( - "net/url" - "slices" - "strings" - - "github.com/dlintw/goconf" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/config" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" -) - -type backendStorageStatic struct { - backendStorageCommon - - logger log.Logger - backendsById map[string]*Backend - - // Deprecated - allowAll bool - commonSecret []byte - compatBackend *Backend -} - -func NewBackendStorageStatic(logger log.Logger, cfg *goconf.ConfigFile, stats BackendStorageStats) (BackendStorage, error) { - allowAll, _ := cfg.GetBool("backend", "allowall") - commonSecret, _ := config.GetStringOptionWithEnv(cfg, "backend", "secret") - backends := make(map[string][]*Backend) - backendsById := make(map[string]*Backend) - var compatBackend *Backend - numBackends := 0 - if allowAll { - logger.Println("WARNING: All backend hostnames are allowed, only use for development!") - compatBackend = NewCompatBackend(cfg) - if sessionLimit := compatBackend.Limit(); sessionLimit > 0 { - logger.Printf("Allow a maximum of %d sessions", sessionLimit) - } - compatBackend.UpdateStats() - backendsById[compatBackend.Id()] = compatBackend - numBackends++ - } else if backendIds, _ := cfg.GetString("backend", "backends"); backendIds != "" { - added := make(map[string]*Backend) - for host, configuredBackends := range getConfiguredHosts(logger, backendIds, cfg, commonSecret) { - backends[host] = append(backends[host], configuredBackends...) - for _, be := range configuredBackends { - added[be.Id()] = be - } - } - for _, be := range added { - logger.Printf("Backend %s added for %s", be.Id(), strings.Join(be.Urls(), ", ")) - backendsById[be.Id()] = be - be.UpdateStats() - be.Count() - } - numBackends += len(added) - } else if allowedUrls, _ := cfg.GetString("backend", "allowed"); allowedUrls != "" { - // Old-style configuration, only hosts are configured and are using a common secret. - allowMap := make(map[string]bool) - for u := range internal.SplitEntries(allowedUrls, ",") { - if idx := strings.IndexByte(u, '/'); idx != -1 { - logger.Printf("WARNING: Removing path from allowed hostname \"%s\", check your configuration!", u) - if u = u[:idx]; u == "" { - continue - } - } - - allowMap[strings.ToLower(u)] = true - } - - if len(allowMap) == 0 { - logger.Println("WARNING: No backend hostnames are allowed, check your configuration!") - } else { - compatBackend = NewCompatBackend(cfg) - hosts := make([]string, 0, len(allowMap)) - for host := range allowMap { - hosts = append(hosts, host) - backends[host] = []*Backend{compatBackend} - } - if len(hosts) > 1 { - logger.Println("WARNING: Using deprecated backend configuration. Please migrate the \"allowed\" setting to the new \"backends\" configuration.") - } - logger.Printf("Allowed backend hostnames: %s", hosts) - if sessionLimit := compatBackend.Limit(); sessionLimit > 0 { - logger.Printf("Allow a maximum of %d sessions", sessionLimit) - } - compatBackend.UpdateStats() - backendsById[compatBackend.Id()] = compatBackend - numBackends++ - } - } - - if numBackends == 0 { - logger.Printf("WARNING: No backends configured, client connections will not be possible.") - } - - stats.AddBackends(numBackends) - return &backendStorageStatic{ - backendStorageCommon: backendStorageCommon{ - backends: backends, - stats: stats, - }, - - logger: logger, - backendsById: backendsById, - - allowAll: allowAll, - commonSecret: []byte(commonSecret), - compatBackend: compatBackend, - }, nil -} - -func (s *backendStorageStatic) Close() { -} - -// +checklocks:s.mu -func (s *backendStorageStatic) RemoveBackendsForHost(host string, seen map[string]seenState) { - if oldBackends := s.backends[host]; len(oldBackends) > 0 { - deleted := 0 - for _, backend := range oldBackends { - if seen[backend.Id()] == seenDeleted { - continue - } - - seen[backend.Id()] = seenDeleted - urls := slices.DeleteFunc(backend.Urls(), func(s string) bool { - return !strings.Contains(s, "://"+host) - }) - s.logger.Printf("Backend %s removed for %s", backend.Id(), strings.Join(urls, ", ")) - if len(urls) == len(backend.Urls()) && backend.Uncount() { - backend.DeleteStats() - delete(s.backendsById, backend.Id()) - deleted++ - } - } - s.stats.RemoveBackends(deleted) - } - delete(s.backends, host) -} - -type seenState int - -const ( - seenNotSeen seenState = iota - seenAdded - seenUpdated - seenDeleted -) - -// +checklocks:s.mu -func (s *backendStorageStatic) UpsertHost(host string, backends []*Backend, seen map[string]seenState) { - for existingIndex, existingBackend := range s.backends[host] { - found := false - index := 0 - for _, newBackend := range backends { - if existingBackend.Equal(newBackend) { - found = true - backends = slices.Delete(backends, index, index+1) - break - } else if newBackend.Id() == existingBackend.Id() { - found = true - s.backends[host][existingIndex] = newBackend - backends = slices.Delete(backends, index, index+1) - if seen[newBackend.Id()] != seenUpdated { - seen[newBackend.Id()] = seenUpdated - s.logger.Printf("Backend %s updated for %s", newBackend.Id(), strings.Join(newBackend.Urls(), ", ")) - newBackend.UpdateStats() - newBackend.CopyCount(existingBackend) - s.backendsById[newBackend.Id()] = newBackend - } - break - } - index++ - } - if !found { - removed := s.backends[host][existingIndex] - s.backends[host] = slices.Delete(s.backends[host], existingIndex, existingIndex+1) - if seen[removed.Id()] != seenDeleted { - seen[removed.Id()] = seenDeleted - urls := slices.DeleteFunc(removed.Urls(), func(s string) bool { - return !strings.Contains(s, "://"+host) - }) - s.logger.Printf("Backend %s removed for %s", removed.Id(), strings.Join(urls, ", ")) - if len(urls) == len(removed.Urls()) && removed.Uncount() { - removed.DeleteStats() - delete(s.backendsById, removed.Id()) - s.stats.DecBackends() - } - } - } - } - - s.backends[host] = append(s.backends[host], backends...) - - addedBackends := 0 - for _, added := range backends { - if seen[added.Id()] == seenAdded { - continue - } - - seen[added.Id()] = seenAdded - if prev, found := s.backendsById[added.Id()]; found { - added.CopyCount(prev) - } else { - s.backendsById[added.Id()] = added - } - - s.logger.Printf("Backend %s added for %s", added.Id(), strings.Join(added.Urls(), ", ")) - if added.Count() { - added.UpdateStats() - addedBackends++ - } - } - s.stats.AddBackends(addedBackends) -} - -func getConfiguredBackendIDs(backendIds string) (ids []string) { - seen := make(map[string]bool) - - for id := range internal.SplitEntries(backendIds, ",") { - if seen[id] { - continue - } - - ids = append(ids, id) - seen[id] = true - } - - return ids -} - -func getConfiguredHosts(logger log.Logger, backendIds string, cfg *goconf.ConfigFile, commonSecret string) (hosts map[string][]*Backend) { - hosts = make(map[string][]*Backend) - seenUrls := make(map[string]string) - for _, id := range getConfiguredBackendIDs(backendIds) { - var urls []string - if u, _ := config.GetStringOptionWithEnv(cfg, id, "urls"); u != "" { - urls = slices.Sorted(internal.SplitEntries(u, ",")) - urls = slices.Compact(urls) - } else if u, _ := config.GetStringOptionWithEnv(cfg, id, "url"); u != "" { - if u = strings.TrimSpace(u); u != "" { - urls = []string{u} - } - } - - if len(urls) == 0 { - logger.Printf("Backend %s is missing or incomplete, skipping", id) - continue - } - - backend, err := NewBackendFromConfig(logger, id, cfg, commonSecret) - if err != nil { - logger.Printf("%s", err) - continue - } - - if sessionLimit := backend.Limit(); sessionLimit > 0 { - logger.Printf("Backend %s allows a maximum of %d sessions", id, sessionLimit) - } - - added := make(map[string]bool) - for _, u := range urls { - if u[len(u)-1] != '/' { - u += "/" - } - - parsed, err := url.Parse(u) - if err != nil { - logger.Printf("Backend %s has an invalid url %s configured (%s), skipping", id, u, err) - continue - } - - var changed bool - if parsed, changed = internal.CanonicalizeUrl(parsed); changed { - u = parsed.String() - } - - if prev, found := seenUrls[u]; found { - logger.Printf("Url %s in backend %s was already used in backend %s, skipping", u, id, prev) - continue - } - - seenUrls[u] = id - backend.AddUrl(parsed) - - if !added[parsed.Host] { - hosts[parsed.Host] = append(hosts[parsed.Host], backend) - added[parsed.Host] = true - } - } - } - - return hosts -} - -func (s *backendStorageStatic) Reload(cfg *goconf.ConfigFile) { - s.mu.Lock() - defer s.mu.Unlock() - - if s.compatBackend != nil { - s.logger.Println("Old-style configuration active, reload is not supported") - return - } - - commonSecret, _ := config.GetStringOptionWithEnv(cfg, "backend", "secret") - - if backendIds, _ := cfg.GetString("backend", "backends"); backendIds != "" { - configuredHosts := getConfiguredHosts(s.logger, backendIds, cfg, commonSecret) - - // remove backends that are no longer configured - seen := make(map[string]seenState) - for hostname := range s.backends { - if _, ok := configuredHosts[hostname]; !ok { - s.RemoveBackendsForHost(hostname, seen) - } - } - - // rewrite backends adding newly configured ones and rewriting existing ones - for hostname, configuredBackends := range configuredHosts { - s.UpsertHost(hostname, configuredBackends, seen) - } - } else { - // remove all backends - seen := make(map[string]seenState) - for hostname := range s.backends { - s.RemoveBackendsForHost(hostname, seen) - } - } -} - -func (s *backendStorageStatic) GetCompatBackend() *Backend { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.compatBackend -} - -func (s *backendStorageStatic) GetBackend(u *url.URL) *Backend { - s.mu.RLock() - defer s.mu.RUnlock() - - if _, found := s.backends[u.Host]; !found { - if s.allowAll { - return s.compatBackend - } - return nil - } - - return s.getBackendLocked(u) -} diff --git a/talk/ocs.go b/talk/ocs.go deleted file mode 100644 index 32b933c..0000000 --- a/talk/ocs.go +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package talk - -import ( - "encoding/json" - "net/url" - "strings" -) - -type OcsMeta struct { - Status string `json:"status"` - StatusCode int `json:"statuscode"` - Message string `json:"message"` -} - -type OcsBody struct { - Meta OcsMeta `json:"meta"` - Data json.RawMessage `json:"data"` -} - -type OcsResponse struct { - json.Marshaler - json.Unmarshaler - - Ocs *OcsBody `json:"ocs"` -} - -func IsOcsRequest(u *url.URL) bool { - return strings.Contains(u.Path, "/ocs/v2.php") || strings.Contains(u.Path, "/ocs/v1.php") -} diff --git a/talk/ocs_easyjson.go b/talk/ocs_easyjson.go deleted file mode 100644 index efcec5f..0000000 --- a/talk/ocs_easyjson.go +++ /dev/null @@ -1,261 +0,0 @@ -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. - -package talk - -import ( - json "encoding/json" - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" -) - -// suppress unused package warning -var ( - _ *json.RawMessage - _ *jlexer.Lexer - _ *jwriter.Writer - _ easyjson.Marshaler -) - -func easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(in *jlexer.Lexer, out *OcsResponse) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "ocs": - if in.IsNull() { - in.Skip() - out.Ocs = nil - } else { - if out.Ocs == nil { - out.Ocs = new(OcsBody) - } - if in.IsNull() { - in.Skip() - } else { - (*out.Ocs).UnmarshalEasyJSON(in) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(out *jwriter.Writer, in OcsResponse) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"ocs\":" - out.RawString(prefix[1:]) - if in.Ocs == nil { - out.RawString("null") - } else { - (*in.Ocs).MarshalEasyJSON(out) - } - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v OcsResponse) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v OcsResponse) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *OcsResponse) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *OcsResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk(l, v) -} -func easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(in *jlexer.Lexer, out *OcsMeta) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "status": - if in.IsNull() { - in.Skip() - } else { - out.Status = string(in.String()) - } - case "statuscode": - if in.IsNull() { - in.Skip() - } else { - out.StatusCode = int(in.Int()) - } - case "message": - if in.IsNull() { - in.Skip() - } else { - out.Message = string(in.String()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(out *jwriter.Writer, in OcsMeta) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"status\":" - out.RawString(prefix[1:]) - out.String(string(in.Status)) - } - { - const prefix string = ",\"statuscode\":" - out.RawString(prefix) - out.Int(int(in.StatusCode)) - } - { - const prefix string = ",\"message\":" - out.RawString(prefix) - out.String(string(in.Message)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v OcsMeta) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v OcsMeta) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *OcsMeta) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *OcsMeta) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk1(l, v) -} -func easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(in *jlexer.Lexer, out *OcsBody) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - switch key { - case "meta": - if in.IsNull() { - in.Skip() - } else { - (out.Meta).UnmarshalEasyJSON(in) - } - case "data": - if in.IsNull() { - in.Skip() - } else { - if data := in.Raw(); in.Ok() { - in.AddError((out.Data).UnmarshalJSON(data)) - } - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(out *jwriter.Writer, in OcsBody) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"meta\":" - out.RawString(prefix[1:]) - (in.Meta).MarshalEasyJSON(out) - } - { - const prefix string = ",\"data\":" - out.RawString(prefix) - out.Raw((in.Data).MarshalJSON()) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v OcsBody) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v OcsBody) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonD5833a6fEncodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *OcsBody) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *OcsBody) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonD5833a6fDecodeGithubComStrukturagNextcloudSpreedSignalingV2Talk2(l, v) -} diff --git a/test/goroutines_test.go b/test/goroutines_test.go deleted file mode 100644 index 65cf390..0000000 --- a/test/goroutines_test.go +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNoGoroutineLeak(t *testing.T) { // nolint:paralleltest - EnsureNoGoroutinesLeak(t, func(t *testing.T) { - stop := make(chan struct{}) - stopped := make(chan struct{}) - - go func() { - defer close(stopped) - <-stop - }() - - close(stop) - <-stopped - }) -} - -func TestLeakGoroutine(t *testing.T) { // nolint:paralleltest - stop := make(chan struct{}) - stopped := make(chan struct{}) - - before, after := ensureNoGoroutinesLeak(t, func(t *testing.T) { - go func() { - defer close(stopped) - <-stop - }() - - }, true) - close(stop) - <-stopped - - assert.Equal(t, 1, after-before, "wrong number of leaked goroutines") -} diff --git a/test/network.go b/test/network.go deleted file mode 100644 index dfc4326..0000000 --- a/test/network.go +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "os" - "runtime" - "syscall" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" -) - -func IsErrorAddressAlreadyInUse(err error) bool { - eOsSyscall, ok := internal.AsErrorType[*os.SyscallError](err) - if !ok { - return false - } - errErrno, ok := internal.AsErrorType[syscall.Errno](eOsSyscall) - if !ok { - return false - } - if errErrno == syscall.EADDRINUSE { - return true - } - const WSAEADDRINUSE = 10048 - if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE { - return true - } - return false -} diff --git a/test/network_test.go b/test/network_test.go deleted file mode 100644 index 69a24fe..0000000 --- a/test/network_test.go +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "net" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestIsErrorAddressAlreadyInUse(t *testing.T) { - t.Parallel() - require := require.New(t) - assert := assert.New(t) - - listener1, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(err) - - listener2, err := net.Listen("tcp", listener1.Addr().String()) - assert.Nil(listener2) - assert.True(IsErrorAddressAlreadyInUse(err), "expected address already in use, got %+v", err) -} diff --git a/test/storage.go b/test/storage.go deleted file mode 100644 index 9b23ad3..0000000 --- a/test/storage.go +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "sync" - "testing" -) - -type Storage[T any] struct { - mu sync.Mutex - // +checklocks:mu - entries map[string]T -} - -func (s *Storage[T]) cleanup(key string) { - s.mu.Lock() - defer s.mu.Unlock() - - delete(s.entries, key) - if len(s.entries) == 0 { - s.entries = nil - } -} - -func (s *Storage[T]) Set(tb testing.TB, value T) { - s.mu.Lock() - defer s.mu.Unlock() - - key := tb.Name() - if _, found := s.entries[key]; !found { - tb.Cleanup(func() { - s.cleanup(key) - }) - } - - if s.entries == nil { - s.entries = make(map[string]T) - } - s.entries[key] = value -} - -func (s *Storage[T]) Get(tb testing.TB) (T, bool) { - s.mu.Lock() - defer s.mu.Unlock() - - key := tb.Name() - if value, found := s.entries[key]; found { - return value, true - } - - var defaultValue T - return defaultValue, false -} - -func (s *Storage[T]) Del(tb testing.TB) { - key := tb.Name() - s.cleanup(key) -} diff --git a/test/storage_test.go b/test/storage_test.go deleted file mode 100644 index 32d6729..0000000 --- a/test/storage_test.go +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_TestStorage(t *testing.T) { - t.Parallel() - assert := assert.New(t) - var storage Storage[int] - - t.Cleanup(func() { - storage.mu.Lock() - defer storage.mu.Unlock() - - assert.Nil(storage.entries) - }) - - v, found := storage.Get(t) - assert.False(found, "expected missing value, got %d", v) - - storage.Set(t, 10) - v, found = storage.Get(t) - assert.True(found) - assert.Equal(10, v) - - storage.Set(t, 20) - v, found = storage.Get(t) - assert.True(found) - assert.Equal(20, v) - - storage.Del(t) - - v, found = storage.Get(t) - assert.False(found, "expected missing value, got %d", v) - - storage.Set(t, 30) - v, found = storage.Get(t) - assert.True(found) - assert.Equal(30, v) -} diff --git a/test/wakeup_channel_test.go b/test/wakeup_channel_test.go deleted file mode 100644 index 12dbc6a..0000000 --- a/test/wakeup_channel_test.go +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG - * - * @author Joachim Bauch - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package test - -import ( - "testing" -) - -func TestDrainWakeupChannel(t *testing.T) { - t.Parallel() - - ch := make(chan struct{}, 2) - ch <- struct{}{} - ch <- struct{}{} - - DrainWakeupChannel(ch) - - ch <- struct{}{} - ch <- struct{}{} -} diff --git a/log/test/log.go b/test_helpers.go similarity index 65% rename from log/test/log.go rename to test_helpers.go index 75b539f..b7f0bdd 100644 --- a/log/test/log.go +++ b/test_helpers.go @@ -19,28 +19,40 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package test +package signaling import ( - stdlog "log" + "io" + "log" "testing" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/test" ) var ( - testLoggers test.Storage[log.Logger] + prevWriter io.Writer + prevFlags int ) -func NewLoggerForTest(t testing.TB) log.Logger { - t.Helper() - - logger, found := testLoggers.Get(t) - if !found { - logger = stdlog.New(t.Output(), t.Name()+": ", stdlog.LstdFlags|stdlog.Lmicroseconds|stdlog.Lshortfile) - - testLoggers.Set(t, logger) - } - return logger +func init() { + prevWriter = log.Writer() + prevFlags = log.Flags() +} + +type testLogWriter struct { + t testing.TB +} + +func (w *testLogWriter) Write(b []byte) (int, error) { + w.t.Helper() + w.t.Logf("%s", string(b)) + return len(b), nil +} + +func CatchLogForTest(t testing.TB) { + t.Cleanup(func() { + log.SetOutput(prevWriter) + log.SetFlags(prevFlags) + }) + + log.SetOutput(&testLogWriter{t}) + log.SetFlags(prevFlags | log.Lshortfile) } diff --git a/testclient_test.go b/testclient_test.go new file mode 100644 index 0000000..9078bcc --- /dev/null +++ b/testclient_test.go @@ -0,0 +1,1130 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2017 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net" + "net/http/httptest" + "reflect" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testBackendSecret = []byte("secret") + testInternalSecret = []byte("internal-secret") + + ErrNoMessageReceived = fmt.Errorf("no message was received by the server") +) + +type TestBackendClientAuthParams struct { + UserId string `json:"userid"` +} + +func getWebsocketUrl(url string) string { + if strings.HasPrefix(url, "http://") { + return "ws://" + url[7:] + "/spreed" + } else if strings.HasPrefix(url, "https://") { + return "wss://" + url[8:] + "/spreed" + } else { + panic("Unsupported URL: " + url) + } +} + +func getPubliceSessionIdData(h *Hub, publicId string) *SessionIdData { + decodedPublic := h.decodePublicSessionId(publicId) + if decodedPublic == nil { + panic("invalid public session id") + } + return decodedPublic +} + +func checkUnexpectedClose(err error) error { + if err != nil && websocket.IsUnexpectedCloseError(err, + websocket.CloseNormalClosure, + websocket.CloseGoingAway, + websocket.CloseNoStatusReceived) { + return fmt.Errorf("Connection was closed with unexpected error: %s", err) + } + + return nil +} + +func checkMessageType(message *ServerMessage, expectedType string) error { + if message == nil { + return ErrNoMessageReceived + } + + if message.Type != expectedType { + return fmt.Errorf("Expected \"%s\" message, got %+v", expectedType, message) + } + switch message.Type { + case "hello": + if message.Hello == nil { + return fmt.Errorf("Expected \"%s\" message, got %+v", expectedType, message) + } + case "message": + if message.Message == nil { + return fmt.Errorf("Expected \"%s\" message, got %+v", expectedType, message) + } else if len(message.Message.Data) == 0 { + return fmt.Errorf("Received message without data") + } + case "room": + if message.Room == nil { + return fmt.Errorf("Expected \"%s\" message, got %+v", expectedType, message) + } + case "event": + if message.Event == nil { + return fmt.Errorf("Expected \"%s\" message, got %+v", expectedType, message) + } + case "transient": + if message.TransientData == nil { + return fmt.Errorf("Expected \"%s\" message, got %+v", expectedType, message) + } + } + + return nil +} + +func checkMessageSender(hub *Hub, sender *MessageServerMessageSender, senderType string, hello *HelloServerMessage) error { + if sender.Type != senderType { + return fmt.Errorf("Expected sender type %s, got %s", senderType, sender.SessionId) + } else if sender.SessionId != hello.SessionId { + return fmt.Errorf("Expected session id %+v, got %+v", + getPubliceSessionIdData(hub, hello.SessionId), getPubliceSessionIdData(hub, sender.SessionId)) + } else if sender.UserId != hello.UserId { + return fmt.Errorf("Expected user id %s, got %s", hello.UserId, sender.UserId) + } + + return nil +} + +func checkReceiveClientMessageWithSenderAndRecipient(ctx context.Context, client *TestClient, senderType string, hello *HelloServerMessage, payload interface{}, sender **MessageServerMessageSender, recipient **MessageClientMessageRecipient) error { + message, err := client.RunUntilMessage(ctx) + if err := checkUnexpectedClose(err); err != nil { + return err + } else if err := checkMessageType(message, "message"); err != nil { + return err + } else if err := checkMessageSender(client.hub, message.Message.Sender, senderType, hello); err != nil { + return err + } else { + if err := json.Unmarshal(message.Message.Data, payload); err != nil { + return err + } + } + if sender != nil { + *sender = message.Message.Sender + } + if recipient != nil { + *recipient = message.Message.Recipient + } + return nil +} + +func checkReceiveClientMessageWithSender(ctx context.Context, client *TestClient, senderType string, hello *HelloServerMessage, payload interface{}, sender **MessageServerMessageSender) error { + return checkReceiveClientMessageWithSenderAndRecipient(ctx, client, senderType, hello, payload, sender, nil) +} + +func checkReceiveClientMessage(ctx context.Context, client *TestClient, senderType string, hello *HelloServerMessage, payload interface{}) error { + return checkReceiveClientMessageWithSenderAndRecipient(ctx, client, senderType, hello, payload, nil, nil) +} + +func checkReceiveClientControlWithSenderAndRecipient(ctx context.Context, client *TestClient, senderType string, hello *HelloServerMessage, payload interface{}, sender **MessageServerMessageSender, recipient **MessageClientMessageRecipient) error { + message, err := client.RunUntilMessage(ctx) + if err := checkUnexpectedClose(err); err != nil { + return err + } else if err := checkMessageType(message, "control"); err != nil { + return err + } else if err := checkMessageSender(client.hub, message.Control.Sender, senderType, hello); err != nil { + return err + } else { + if err := json.Unmarshal(message.Control.Data, payload); err != nil { + return err + } + } + if sender != nil { + *sender = message.Control.Sender + } + if recipient != nil { + *recipient = message.Control.Recipient + } + return nil +} + +func checkReceiveClientControlWithSender(ctx context.Context, client *TestClient, senderType string, hello *HelloServerMessage, payload interface{}, sender **MessageServerMessageSender) error { // nolint + return checkReceiveClientControlWithSenderAndRecipient(ctx, client, senderType, hello, payload, sender, nil) +} + +func checkReceiveClientControl(ctx context.Context, client *TestClient, senderType string, hello *HelloServerMessage, payload interface{}) error { + return checkReceiveClientControlWithSenderAndRecipient(ctx, client, senderType, hello, payload, nil, nil) +} + +func checkReceiveClientEvent(ctx context.Context, client *TestClient, eventType string, msg **EventServerMessage) error { + message, err := client.RunUntilMessage(ctx) + if err := checkUnexpectedClose(err); err != nil { + return err + } else if err := checkMessageType(message, "event"); err != nil { + return err + } else if message.Event.Type != eventType { + return fmt.Errorf("Expected \"%s\" event type, got \"%s\"", eventType, message.Event.Type) + } else { + if msg != nil { + *msg = message.Event + } + } + return nil +} + +type TestClient struct { + t *testing.T + hub *Hub + server *httptest.Server + + mu sync.Mutex + conn *websocket.Conn + localAddr net.Addr + + messageChan chan []byte + readErrorChan chan error + + publicId string +} + +func NewTestClientContext(ctx context.Context, t *testing.T, server *httptest.Server, hub *Hub) *TestClient { + // Reference "hub" to prevent compiler error. + conn, _, err := websocket.DefaultDialer.DialContext(ctx, getWebsocketUrl(server.URL), nil) + require.NoError(t, err) + + messageChan := make(chan []byte) + readErrorChan := make(chan error, 1) + + go func() { + for { + messageType, data, err := conn.ReadMessage() + if err != nil { + readErrorChan <- err + return + } else if !assert.Equal(t, websocket.TextMessage, messageType) { + return + } + + messageChan <- data + } + }() + + return &TestClient{ + t: t, + hub: hub, + server: server, + + conn: conn, + localAddr: conn.LocalAddr(), + + messageChan: messageChan, + readErrorChan: readErrorChan, + } +} + +func NewTestClient(t *testing.T, server *httptest.Server, hub *Hub) *TestClient { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + client := NewTestClientContext(ctx, t, server, hub) + msg, err := client.RunUntilMessage(ctx) + require.NoError(t, err) + assert.Equal(t, "welcome", msg.Type) + return client +} + +func (c *TestClient) CloseWithBye() { + c.SendBye() // nolint + c.Close() +} + +func (c *TestClient) Close() { + c.mu.Lock() + defer c.mu.Unlock() + if err := c.conn.WriteMessage(websocket.CloseMessage, []byte{}); err == websocket.ErrCloseSent { + // Already closed + return + } + + // Wait a bit for close message to be processed. + time.Sleep(100 * time.Millisecond) + c.conn.Close() + + // Drain any entries in the channels to terminate the read goroutine. +loop: + for { + select { + case <-c.readErrorChan: + case <-c.messageChan: + default: + break loop + } + } +} + +func (c *TestClient) WaitForClientRemoved(ctx context.Context) error { + c.hub.mu.Lock() + defer c.hub.mu.Unlock() + for { + found := false + for _, client := range c.hub.clients { + if cc, ok := client.(*Client); ok { + cc.mu.Lock() + conn := cc.conn + cc.mu.Unlock() + if conn != nil && conn.RemoteAddr().String() == c.localAddr.String() { + found = true + break + } + } + } + if !found { + break + } + + c.hub.mu.Unlock() + select { + case <-ctx.Done(): + c.hub.mu.Lock() + return ctx.Err() + default: + time.Sleep(time.Millisecond) + } + c.hub.mu.Lock() + } + return nil +} + +func (c *TestClient) WaitForSessionRemoved(ctx context.Context, sessionId string) error { + data := c.hub.decodePublicSessionId(sessionId) + if data == nil { + return fmt.Errorf("Invalid session id passed") + } + + c.hub.mu.Lock() + defer c.hub.mu.Unlock() + for { + _, found := c.hub.sessions[data.Sid] + if !found { + break + } + + c.hub.mu.Unlock() + select { + case <-ctx.Done(): + c.hub.mu.Lock() + return ctx.Err() + default: + time.Sleep(time.Millisecond) + } + c.hub.mu.Lock() + } + return nil +} + +func (c *TestClient) WriteJSON(data interface{}) error { + if !strings.Contains(c.t.Name(), "HelloUnsupportedVersion") { + if msg, ok := data.(*ClientMessage); ok { + if err := msg.CheckValid(); err != nil { + return err + } + } + } + + c.mu.Lock() + defer c.mu.Unlock() + return c.conn.WriteJSON(data) +} + +func (c *TestClient) EnsuerWriteJSON(data interface{}) { + require.NoError(c.t, c.WriteJSON(data), "Could not write JSON %+v", data) +} + +func (c *TestClient) SendHello(userid string) error { + return c.SendHelloV1(userid) +} + +func (c *TestClient) SendHelloV1(userid string) error { + params := TestBackendClientAuthParams{ + UserId: userid, + } + return c.SendHelloParams(c.server.URL, HelloVersionV1, "", nil, params) +} + +func (c *TestClient) SendHelloV2(userid string) error { + return c.SendHelloV2WithFeatures(userid, nil) +} + +func (c *TestClient) SendHelloV2WithFeatures(userid string, features []string) error { + now := time.Now() + return c.SendHelloV2WithTimesAndFeatures(userid, now, now.Add(time.Minute), features) +} + +func (c *TestClient) CreateHelloV2TokenWithUserdata(userid string, issuedAt time.Time, expiresAt time.Time, userdata map[string]interface{}) (string, error) { + data, err := json.Marshal(userdata) + if err != nil { + return "", err + } + + claims := &HelloV2TokenClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: c.server.URL, + Subject: userid, + }, + UserData: data, + } + if !issuedAt.IsZero() { + claims.IssuedAt = jwt.NewNumericDate(issuedAt) + } + if !expiresAt.IsZero() { + claims.ExpiresAt = jwt.NewNumericDate(expiresAt) + } + + var token *jwt.Token + if strings.Contains(c.t.Name(), "ECDSA") { + token = jwt.NewWithClaims(jwt.SigningMethodES256, claims) + } else if strings.Contains(c.t.Name(), "Ed25519") { + token = jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims) + } else { + token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + } + private := getPrivateAuthToken(c.t) + return token.SignedString(private) +} + +func (c *TestClient) CreateHelloV2Token(userid string, issuedAt time.Time, expiresAt time.Time) (string, error) { + userdata := map[string]interface{}{ + "displayname": "Displayname " + userid, + } + + return c.CreateHelloV2TokenWithUserdata(userid, issuedAt, expiresAt, userdata) +} + +func (c *TestClient) SendHelloV2WithTimes(userid string, issuedAt time.Time, expiresAt time.Time) error { + return c.SendHelloV2WithTimesAndFeatures(userid, issuedAt, expiresAt, nil) +} + +func (c *TestClient) SendHelloV2WithTimesAndFeatures(userid string, issuedAt time.Time, expiresAt time.Time, features []string) error { + tokenString, err := c.CreateHelloV2Token(userid, issuedAt, expiresAt) + require.NoError(c.t, err) + + params := HelloV2AuthParams{ + Token: tokenString, + } + return c.SendHelloParams(c.server.URL, HelloVersionV2, "", features, params) +} + +func (c *TestClient) SendHelloResume(resumeId string) error { + hello := &ClientMessage{ + Id: "1234", + Type: "hello", + Hello: &HelloClientMessage{ + Version: HelloVersionV1, + ResumeId: resumeId, + }, + } + return c.WriteJSON(hello) +} + +func (c *TestClient) SendHelloClient(userid string) error { + return c.SendHelloClientWithFeatures(userid, nil) +} + +func (c *TestClient) SendHelloClientWithFeatures(userid string, features []string) error { + params := TestBackendClientAuthParams{ + UserId: userid, + } + return c.SendHelloParams(c.server.URL, HelloVersionV1, "client", features, params) +} + +func (c *TestClient) SendHelloInternal() error { + return c.SendHelloInternalWithFeatures(nil) +} + +func (c *TestClient) SendHelloInternalWithFeatures(features []string) error { + random := newRandomString(48) + mac := hmac.New(sha256.New, testInternalSecret) + mac.Write([]byte(random)) // nolint + token := hex.EncodeToString(mac.Sum(nil)) + backend := c.server.URL + + params := ClientTypeInternalAuthParams{ + Random: random, + Token: token, + Backend: backend, + } + return c.SendHelloParams("", HelloVersionV1, "internal", features, params) +} + +func (c *TestClient) SendHelloParams(url string, version string, clientType string, features []string, params interface{}) error { + data, err := json.Marshal(params) + require.NoError(c.t, err) + + hello := &ClientMessage{ + Id: "1234", + Type: "hello", + Hello: &HelloClientMessage{ + Version: version, + Features: features, + Auth: &HelloClientMessageAuth{ + Type: clientType, + Url: url, + Params: data, + }, + }, + } + return c.WriteJSON(hello) +} + +func (c *TestClient) SendBye() error { + hello := &ClientMessage{ + Id: "9876", + Type: "bye", + Bye: &ByeClientMessage{}, + } + return c.WriteJSON(hello) +} + +func (c *TestClient) SendMessage(recipient MessageClientMessageRecipient, data interface{}) error { + payload, err := json.Marshal(data) + require.NoError(c.t, err) + + message := &ClientMessage{ + Id: "abcd", + Type: "message", + Message: &MessageClientMessage{ + Recipient: recipient, + Data: payload, + }, + } + return c.WriteJSON(message) +} + +func (c *TestClient) SendControl(recipient MessageClientMessageRecipient, data interface{}) error { + payload, err := json.Marshal(data) + require.NoError(c.t, err) + + message := &ClientMessage{ + Id: "abcd", + Type: "control", + Control: &ControlClientMessage{ + MessageClientMessage: MessageClientMessage{ + Recipient: recipient, + Data: payload, + }, + }, + } + return c.WriteJSON(message) +} + +func (c *TestClient) SendInternalAddSession(msg *AddSessionInternalClientMessage) error { + message := &ClientMessage{ + Id: "abcd", + Type: "internal", + Internal: &InternalClientMessage{ + Type: "addsession", + AddSession: msg, + }, + } + return c.WriteJSON(message) +} + +func (c *TestClient) SendInternalUpdateSession(msg *UpdateSessionInternalClientMessage) error { + message := &ClientMessage{ + Id: "abcd", + Type: "internal", + Internal: &InternalClientMessage{ + Type: "updatesession", + UpdateSession: msg, + }, + } + return c.WriteJSON(message) +} + +func (c *TestClient) SendInternalRemoveSession(msg *RemoveSessionInternalClientMessage) error { + message := &ClientMessage{ + Id: "abcd", + Type: "internal", + Internal: &InternalClientMessage{ + Type: "removesession", + RemoveSession: msg, + }, + } + return c.WriteJSON(message) +} + +func (c *TestClient) SendInternalDialout(msg *DialoutInternalClientMessage) error { + message := &ClientMessage{ + Id: "abcd", + Type: "internal", + Internal: &InternalClientMessage{ + Type: "dialout", + Dialout: msg, + }, + } + return c.WriteJSON(message) +} + +func (c *TestClient) SetTransientData(key string, value interface{}, ttl time.Duration) error { + payload, err := json.Marshal(value) + require.NoError(c.t, err) + + message := &ClientMessage{ + Id: "efgh", + Type: "transient", + TransientData: &TransientDataClientMessage{ + Type: "set", + Key: key, + Value: payload, + TTL: ttl, + }, + } + return c.WriteJSON(message) +} + +func (c *TestClient) RemoveTransientData(key string) error { + message := &ClientMessage{ + Id: "ijkl", + Type: "transient", + TransientData: &TransientDataClientMessage{ + Type: "remove", + Key: key, + }, + } + return c.WriteJSON(message) +} + +func (c *TestClient) DrainMessages(ctx context.Context) error { + select { + case err := <-c.readErrorChan: + return err + case <-c.messageChan: + n := len(c.messageChan) + for i := 0; i < n; i++ { + <-c.messageChan + } + case <-ctx.Done(): + return ctx.Err() + } + return nil +} + +func (c *TestClient) GetPendingMessages(ctx context.Context) ([]*ServerMessage, error) { + var result []*ServerMessage + select { + case err := <-c.readErrorChan: + return nil, err + case msg := <-c.messageChan: + var m ServerMessage + if err := json.Unmarshal(msg, &m); err != nil { + return nil, err + } + result = append(result, &m) + n := len(c.messageChan) + for i := 0; i < n; i++ { + var m ServerMessage + msg = <-c.messageChan + if err := json.Unmarshal(msg, &m); err != nil { + return nil, err + } + result = append(result, &m) + } + case <-ctx.Done(): + return nil, ctx.Err() + } + return result, nil +} + +func (c *TestClient) RunUntilMessage(ctx context.Context) (message *ServerMessage, err error) { + select { + case err = <-c.readErrorChan: + case msg := <-c.messageChan: + var m ServerMessage + if err = json.Unmarshal(msg, &m); err == nil { + message = &m + } + case <-ctx.Done(): + err = ctx.Err() + } + return +} + +func (c *TestClient) RunUntilHello(ctx context.Context) (message *ServerMessage, err error) { + if message, err = c.RunUntilMessage(ctx); err != nil { + return nil, err + } + if err := checkUnexpectedClose(err); err != nil { + return nil, err + } + if err := checkMessageType(message, "hello"); err != nil { + return nil, err + } + c.publicId = message.Hello.SessionId + return message, nil +} + +func (c *TestClient) JoinRoom(ctx context.Context, roomId string) (message *ServerMessage, err error) { + return c.JoinRoomWithRoomSession(ctx, roomId, roomId+"-"+c.publicId) +} + +func (c *TestClient) JoinRoomWithRoomSession(ctx context.Context, roomId string, roomSessionId string) (message *ServerMessage, err error) { + msg := &ClientMessage{ + Id: "ABCD", + Type: "room", + Room: &RoomClientMessage{ + RoomId: roomId, + SessionId: roomSessionId, + }, + } + if err := c.WriteJSON(msg); err != nil { + return nil, err + } + + if message, err = c.RunUntilMessage(ctx); err != nil { + return nil, err + } + if err := checkUnexpectedClose(err); err != nil { + return nil, err + } + if err := checkMessageType(message, "room"); err != nil { + return nil, err + } + if message.Id != msg.Id { + return nil, fmt.Errorf("expected message id %s, got %s", msg.Id, message.Id) + } + return message, nil +} + +func checkMessageRoomId(message *ServerMessage, roomId string) error { + if err := checkMessageType(message, "room"); err != nil { + return err + } + if message.Room.RoomId != roomId { + return fmt.Errorf("Expected room id %s, got %+v", roomId, message.Room) + } + return nil +} + +func (c *TestClient) RunUntilRoom(ctx context.Context, roomId string) error { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return err + } + if err := checkUnexpectedClose(err); err != nil { + return err + } + return checkMessageRoomId(message, roomId) +} + +func (c *TestClient) checkMessageJoined(message *ServerMessage, hello *HelloServerMessage) error { + return c.checkMessageJoinedSession(message, hello.SessionId, hello.UserId) +} + +func (c *TestClient) checkSingleMessageJoined(message *ServerMessage) error { + if err := checkMessageType(message, "event"); err != nil { + return err + } else if message.Event.Target != "room" { + return fmt.Errorf("Expected event target room, got %+v", message.Event) + } else if message.Event.Type != "join" { + return fmt.Errorf("Expected event type join, got %+v", message.Event) + } else if len(message.Event.Join) != 1 { + return fmt.Errorf("Expected one join event entry, got %+v", message.Event) + } + return nil +} + +func (c *TestClient) checkMessageJoinedSession(message *ServerMessage, sessionId string, userId string) error { + if err := c.checkSingleMessageJoined(message); err != nil { + return err + } + + evt := message.Event.Join[0] + if sessionId != "" && evt.SessionId != sessionId { + return fmt.Errorf("Expected join session id %+v, got %+v", + getPubliceSessionIdData(c.hub, sessionId), getPubliceSessionIdData(c.hub, evt.SessionId)) + } + if evt.UserId != userId { + return fmt.Errorf("Expected join user id %s, got %+v", userId, evt) + } + return nil +} + +func (c *TestClient) RunUntilJoinedAndReturn(ctx context.Context, hello ...*HelloServerMessage) ([]*EventServerMessageSessionEntry, []*ServerMessage, error) { + received := make([]*EventServerMessageSessionEntry, len(hello)) + var ignored []*ServerMessage + hellos := make(map[*HelloServerMessage]int, len(hello)) + for idx, h := range hello { + hellos[h] = idx + } + for len(hellos) > 0 { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return nil, nil, fmt.Errorf("got error while waiting for %+v: %w", hellos, err) + } + + if err := checkMessageType(message, "event"); err != nil { + ignored = append(ignored, message) + continue + } else if message.Event.Target != "room" || message.Event.Type != "join" { + ignored = append(ignored, message) + continue + } + + for len(message.Event.Join) > 0 { + found := false + loop: + for h, idx := range hellos { + for idx2, evt := range message.Event.Join { + if evt.SessionId == h.SessionId && evt.UserId == h.UserId { + received[idx] = evt + delete(hellos, h) + message.Event.Join = append(message.Event.Join[:idx2], message.Event.Join[idx2+1:]...) + found = true + break loop + } + } + } + if !found { + return nil, nil, fmt.Errorf("expected one of the passed hello sessions, got %+v", message.Event.Join[0]) + } + } + } + return received, ignored, nil +} + +func (c *TestClient) RunUntilJoined(ctx context.Context, hello ...*HelloServerMessage) error { + _, unexpected, err := c.RunUntilJoinedAndReturn(ctx, hello...) + if err != nil { + return err + } + if len(unexpected) > 0 { + return fmt.Errorf("Received unexpected messages: %+v", unexpected) + } + return nil +} + +func (c *TestClient) checkMessageRoomLeave(message *ServerMessage, hello *HelloServerMessage) error { + return c.checkMessageRoomLeaveSession(message, hello.SessionId) +} + +func (c *TestClient) checkMessageRoomLeaveSession(message *ServerMessage, sessionId string) error { + if err := checkMessageType(message, "event"); err != nil { + return err + } else if message.Event.Target != "room" { + return fmt.Errorf("Expected event target room, got %+v", message.Event) + } else if message.Event.Type != "leave" { + return fmt.Errorf("Expected event type leave, got %+v", message.Event) + } else if len(message.Event.Leave) != 1 { + return fmt.Errorf("Expected one leave event entry, got %+v", message.Event) + } else if message.Event.Leave[0] != sessionId { + return fmt.Errorf("Expected leave session id %+v, got %+v", + getPubliceSessionIdData(c.hub, sessionId), getPubliceSessionIdData(c.hub, message.Event.Leave[0])) + } + return nil +} + +func (c *TestClient) RunUntilLeft(ctx context.Context, hello *HelloServerMessage) error { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return err + } + + return c.checkMessageRoomLeave(message, hello) +} + +func checkMessageRoomlistUpdate(message *ServerMessage) (*RoomEventServerMessage, error) { + if err := checkMessageType(message, "event"); err != nil { + return nil, err + } else if message.Event.Target != "roomlist" { + return nil, fmt.Errorf("Expected event target room, got %+v", message.Event) + } else if message.Event.Type != "update" || message.Event.Update == nil { + return nil, fmt.Errorf("Expected event type update, got %+v", message.Event) + } else { + return message.Event.Update, nil + } +} + +func (c *TestClient) RunUntilRoomlistUpdate(ctx context.Context) (*RoomEventServerMessage, error) { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return nil, err + } + + return checkMessageRoomlistUpdate(message) +} + +func checkMessageRoomlistDisinvite(message *ServerMessage) (*RoomDisinviteEventServerMessage, error) { + if err := checkMessageType(message, "event"); err != nil { + return nil, err + } else if message.Event.Target != "roomlist" { + return nil, fmt.Errorf("Expected event target room, got %+v", message.Event) + } else if message.Event.Type != "disinvite" || message.Event.Disinvite == nil { + return nil, fmt.Errorf("Expected event type disinvite, got %+v", message.Event) + } + + return message.Event.Disinvite, nil +} + +func (c *TestClient) RunUntilRoomlistDisinvite(ctx context.Context) (*RoomDisinviteEventServerMessage, error) { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return nil, err + } + + return checkMessageRoomlistDisinvite(message) +} + +func checkMessageParticipantsInCall(message *ServerMessage) (*RoomEventServerMessage, error) { + if err := checkMessageType(message, "event"); err != nil { + return nil, err + } else if message.Event.Target != "participants" { + return nil, fmt.Errorf("Expected event target participants, got %+v", message.Event) + } else if message.Event.Type != "update" || message.Event.Update == nil { + return nil, fmt.Errorf("Expected event type update, got %+v", message.Event) + } + + return message.Event.Update, nil +} + +func checkMessageParticipantFlags(message *ServerMessage) (*RoomFlagsServerMessage, error) { + if err := checkMessageType(message, "event"); err != nil { + return nil, err + } else if message.Event.Target != "participants" { + return nil, fmt.Errorf("Expected event target room, got %+v", message.Event) + } else if message.Event.Type != "flags" || message.Event.Flags == nil { + return nil, fmt.Errorf("Expected event type flags, got %+v", message.Event) + } + + return message.Event.Flags, nil +} + +func checkMessageRoomMessage(message *ServerMessage) (*RoomEventMessage, error) { + if err := checkMessageType(message, "event"); err != nil { + return nil, err + } else if message.Event.Target != "room" { + return nil, fmt.Errorf("Expected event target room, got %+v", message.Event) + } else if message.Event.Type != "message" || message.Event.Message == nil { + return nil, fmt.Errorf("Expected event type message, got %+v", message.Event) + } + + return message.Event.Message, nil +} + +func (c *TestClient) RunUntilRoomMessage(ctx context.Context) (*RoomEventMessage, error) { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return nil, err + } + + return checkMessageRoomMessage(message) +} + +func checkMessageError(message *ServerMessage, msgid string) error { + if err := checkMessageType(message, "error"); err != nil { + return err + } else if message.Error.Code != msgid { + return fmt.Errorf("Expected error \"%s\", got \"%s\" (%+v)", msgid, message.Error.Code, message.Error) + } + + return nil +} + +func (c *TestClient) RunUntilOffer(ctx context.Context, offer string) error { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return err + } + if err := checkUnexpectedClose(err); err != nil { + return err + } else if err := checkMessageType(message, "message"); err != nil { + return err + } + + var data map[string]interface{} + if err := json.Unmarshal(message.Message.Data, &data); err != nil { + return err + } + + if data["type"].(string) != "offer" { + return fmt.Errorf("expected data type offer, got %+v", data) + } + + payload := data["payload"].(map[string]interface{}) + if payload["type"].(string) != "offer" { + return fmt.Errorf("expected payload type offer, got %+v", payload) + } + if payload["sdp"].(string) != offer { + return fmt.Errorf("expected payload answer %s, got %+v", offer, payload) + } + + return nil +} + +func (c *TestClient) RunUntilAnswer(ctx context.Context, answer string) error { + return c.RunUntilAnswerFromSender(ctx, answer, nil) +} + +func (c *TestClient) RunUntilAnswerFromSender(ctx context.Context, answer string, sender *MessageServerMessageSender) error { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return err + } + if err := checkUnexpectedClose(err); err != nil { + return err + } else if err := checkMessageType(message, "message"); err != nil { + return err + } + + if sender != nil { + if err := checkMessageSender(c.hub, message.Message.Sender, sender.Type, &HelloServerMessage{ + SessionId: sender.SessionId, + UserId: sender.UserId, + }); err != nil { + return err + } + } + + var data map[string]interface{} + if err := json.Unmarshal(message.Message.Data, &data); err != nil { + return err + } + + if data["type"].(string) != "answer" { + return fmt.Errorf("expected data type answer, got %+v", data) + } + + payload := data["payload"].(map[string]interface{}) + if payload["type"].(string) != "answer" { + return fmt.Errorf("expected payload type answer, got %+v", payload) + } + if payload["sdp"].(string) != answer { + return fmt.Errorf("expected payload answer %s, got %+v", answer, payload) + } + + return nil +} + +func checkMessageTransientSet(message *ServerMessage, key string, value interface{}, oldValue interface{}) error { + if err := checkMessageType(message, "transient"); err != nil { + return err + } else if message.TransientData.Type != "set" { + return fmt.Errorf("Expected transient set, got %+v", message.TransientData) + } else if message.TransientData.Key != key { + return fmt.Errorf("Expected transient set key %s, got %+v", key, message.TransientData) + } else if !reflect.DeepEqual(message.TransientData.Value, value) { + return fmt.Errorf("Expected transient set value %+v, got %+v", value, message.TransientData.Value) + } else if !reflect.DeepEqual(message.TransientData.OldValue, oldValue) { + return fmt.Errorf("Expected transient set old value %+v, got %+v", oldValue, message.TransientData.OldValue) + } + + return nil +} + +func checkMessageTransientRemove(message *ServerMessage, key string, oldValue interface{}) error { + if err := checkMessageType(message, "transient"); err != nil { + return err + } else if message.TransientData.Type != "remove" { + return fmt.Errorf("Expected transient remove, got %+v", message.TransientData) + } else if message.TransientData.Key != key { + return fmt.Errorf("Expected transient remove key %s, got %+v", key, message.TransientData) + } else if !reflect.DeepEqual(message.TransientData.OldValue, oldValue) { + return fmt.Errorf("Expected transient remove old value %+v, got %+v", oldValue, message.TransientData.OldValue) + } + + return nil +} + +func checkMessageTransientInitial(message *ServerMessage, data map[string]interface{}) error { + if err := checkMessageType(message, "transient"); err != nil { + return err + } else if message.TransientData.Type != "initial" { + return fmt.Errorf("Expected transient initial, got %+v", message.TransientData) + } else if !reflect.DeepEqual(message.TransientData.Data, data) { + return fmt.Errorf("Expected transient initial data %+v, got %+v", data, message.TransientData.Data) + } + + return nil +} + +func checkMessageInCallAll(message *ServerMessage, roomId string, inCall int) error { + if err := checkMessageType(message, "event"); err != nil { + return err + } else if message.Event.Type != "update" { + return fmt.Errorf("Expected update event, got %+v", message.Event) + } else if message.Event.Target != "participants" { + return fmt.Errorf("Expected participants update event, got %+v", message.Event) + } else if message.Event.Update.RoomId != roomId { + return fmt.Errorf("Expected participants update event for room %s, got %+v", roomId, message.Event.Update) + } else if !message.Event.Update.All { + return fmt.Errorf("Expected participants update event for all, got %+v", message.Event.Update) + } else if !bytes.Equal(message.Event.Update.InCall, []byte(strconv.FormatInt(int64(inCall), 10))) { + return fmt.Errorf("Expected incall flags %d, got %+v", inCall, message.Event.Update) + } + return nil +} + +func checkMessageSwitchTo(message *ServerMessage, roomId string, details json.RawMessage) (*EventServerMessageSwitchTo, error) { + if err := checkMessageType(message, "event"); err != nil { + return nil, err + } else if message.Event.Type != "switchto" { + return nil, fmt.Errorf("Expected switchto event, got %+v", message.Event) + } else if message.Event.Target != "room" { + return nil, fmt.Errorf("Expected room switchto event, got %+v", message.Event) + } else if message.Event.SwitchTo.RoomId != roomId { + return nil, fmt.Errorf("Expected room switchto event for room %s, got %+v", roomId, message.Event) + } + if details != nil { + if message.Event.SwitchTo.Details == nil || !bytes.Equal(details, message.Event.SwitchTo.Details) { + return nil, fmt.Errorf("Expected details %s, got %+v", string(details), message.Event) + } + } else if message.Event.SwitchTo.Details != nil { + return nil, fmt.Errorf("Expected no details, got %+v", message.Event) + } + return message.Event.SwitchTo, nil +} + +func (c *TestClient) RunUntilSwitchTo(ctx context.Context, roomId string, details json.RawMessage) (*EventServerMessageSwitchTo, error) { + message, err := c.RunUntilMessage(ctx) + if err != nil { + return nil, err + } + + return checkMessageSwitchTo(message, roomId, details) +} diff --git a/test/goroutines.go b/testutils_test.go similarity index 76% rename from test/goroutines.go rename to testutils_test.go index e062e24..f2d507a 100644 --- a/test/goroutines.go +++ b/testutils_test.go @@ -1,6 +1,6 @@ /** * Standalone signaling server for the Nextcloud Spreed app. - * Copyright (C) 2025 struktur AG + * Copyright (C) 2021 struktur AG * * @author Joachim Bauch * @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package test +package signaling import ( "bytes" @@ -36,13 +36,7 @@ import ( var listenSignalOnce sync.Once -func EnsureNoGoroutinesLeak(t *testing.T, f func(t *testing.T)) { - t.Helper() - - ensureNoGoroutinesLeak(t, f, false) -} - -func ensureNoGoroutinesLeak(t *testing.T, f func(t *testing.T), fromTest bool) (int, int) { +func ensureNoGoroutinesLeak(t *testing.T, f func(t *testing.T)) { t.Helper() // Make sure test is not executed with "t.Parallel()" t.Setenv("PARALLEL_CHECK", "1") @@ -63,24 +57,17 @@ func ensureNoGoroutinesLeak(t *testing.T, f func(t *testing.T), fromTest bool) ( profile := pprof.Lookup("goroutine") // Give time for things to settle before capturing the number of // go routines - var before int - timeout := time.Now().Add(time.Second) - for time.Now().Before(timeout) { - before = profile.Count() - time.Sleep(10 * time.Millisecond) - if profile.Count() == before { - break - } - } + time.Sleep(500 * time.Millisecond) + before := profile.Count() var prev bytes.Buffer - DumpGoroutines("Before:", &prev) + dumpGoroutines("Before:", &prev) t.Run("leakcheck", f) var after int // Give time for things to settle before capturing the number of // go routines - timeout = time.Now().Add(time.Second) + timeout := time.Now().Add(time.Second) for time.Now().Before(timeout) { after = profile.Count() if after == before { @@ -88,15 +75,14 @@ func ensureNoGoroutinesLeak(t *testing.T, f func(t *testing.T), fromTest bool) ( } } - if after != before && !fromTest { + if after != before { io.Copy(os.Stderr, &prev) // nolint - DumpGoroutines("After:", os.Stderr) + dumpGoroutines("After:", os.Stderr) require.Equal(t, before, after, "Number of Go routines has changed") } - return before, after } -func DumpGoroutines(prefix string, w io.Writer) { +func dumpGoroutines(prefix string, w io.Writer) { if prefix != "" { io.WriteString(w, prefix+"\n") // nolint } diff --git a/async/throttle.go b/throttle.go similarity index 87% rename from async/throttle.go rename to throttle.go index 7b1ebe5..dcd5332 100644 --- a/async/throttle.go +++ b/throttle.go @@ -19,18 +19,16 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package async +package signaling import ( "context" "errors" + "log" "net" "strconv" "sync" "time" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" ) const ( @@ -92,33 +90,25 @@ type throttleEntry struct { ts time.Time } -type GetTimeFunc func() time.Time -type ThrottleDelayFunc func(context.Context, time.Duration) - type memoryThrottler struct { - getNow GetTimeFunc - doDelay ThrottleDelayFunc + getNow func() time.Time + doDelay func(context.Context, time.Duration) - mu sync.RWMutex - // +checklocks:mu + mu sync.RWMutex clients map[string]map[string][]throttleEntry - closer *internal.Closer + closer *Closer } func NewMemoryThrottler() (Throttler, error) { - return NewCustomMemoryThrottler(time.Now, defaultDelay) -} - -func NewCustomMemoryThrottler(getNow GetTimeFunc, delay ThrottleDelayFunc) (Throttler, error) { result := &memoryThrottler{ - getNow: getNow, - doDelay: delay, + getNow: time.Now, clients: make(map[string]map[string][]throttleEntry), - closer: internal.NewCloser(), + closer: NewCloser(), } + result.doDelay = result.delay go result.housekeeping() return result, nil } @@ -267,7 +257,10 @@ func (t *memoryThrottler) getDelay(count int) time.Duration { return maxThrottleDelay } - delay := min(time.Duration(100*intPow(2, count))*time.Millisecond, maxThrottleDelay) + delay := time.Duration(100*intPow(2, count)) * time.Millisecond + if delay > maxThrottleDelay { + delay = maxThrottleDelay + } return delay } @@ -286,8 +279,7 @@ func (t *memoryThrottler) CheckBruteforce(ctx context.Context, client string, ac if l >= maxBruteforceAttempts { delta := now.Sub(entries[l-maxBruteforceAttempts].ts) if delta <= maxBruteforceDurationThreshold { - logger := log.LoggerFromContext(ctx) - logger.Printf("Detected bruteforce attempt on \"%s\" from %s", action, client) + log.Printf("Detected bruteforce attempt on \"%s\" from %s", action, client) statsThrottleBruteforceTotal.WithLabelValues(action).Inc() return doThrottle, ErrBruteforceDetected } @@ -311,13 +303,12 @@ func (t *memoryThrottler) throttle(ctx context.Context, client string, action st } count := t.addEntry(client, action, entry) delay := t.getDelay(count - 1) - logger := log.LoggerFromContext(ctx) - logger.Printf("Failed attempt on \"%s\" from %s, throttling by %s", action, client, delay) + log.Printf("Failed attempt on \"%s\" from %s, throttling by %s", action, client, delay) statsThrottleDelayedTotal.WithLabelValues(action, strconv.FormatInt(delay.Milliseconds(), 10)).Inc() t.doDelay(ctx, delay) } -func defaultDelay(ctx context.Context, duration time.Duration) { +func (t *memoryThrottler) delay(ctx context.Context, duration time.Duration) { c, cancel := context.WithTimeout(ctx, duration) defer cancel() diff --git a/async/throttle_stats_prometheus.go b/throttle_stats_prometheus.go similarity index 93% rename from async/throttle_stats_prometheus.go rename to throttle_stats_prometheus.go index 64c140b..8279fe0 100644 --- a/async/throttle_stats_prometheus.go +++ b/throttle_stats_prometheus.go @@ -19,12 +19,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package async +package signaling import ( "github.com/prometheus/client_golang/prometheus" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/metrics" ) var ( @@ -49,5 +47,5 @@ var ( ) func RegisterThrottleStats() { - metrics.RegisterAll(throttleStats...) + registerAll(throttleStats...) } diff --git a/throttle_test.go b/throttle_test.go new file mode 100644 index 0000000..a95102a --- /dev/null +++ b/throttle_test.go @@ -0,0 +1,311 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2024 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newMemoryThrottlerForTest(t *testing.T) *memoryThrottler { + t.Helper() + result, err := NewMemoryThrottler() + require.NoError(t, err) + + t.Cleanup(func() { + result.Close() + }) + + return result.(*memoryThrottler) +} + +type throttlerTiming struct { + t *testing.T + + now time.Time + expectedSleep time.Duration +} + +func (t *throttlerTiming) getNow() time.Time { + return t.now +} + +func (t *throttlerTiming) doDelay(ctx context.Context, duration time.Duration) { + t.t.Helper() + assert.Equal(t.t, t.expectedSleep, duration) +} + +func TestThrottler(t *testing.T) { + assert := assert.New(t) + timing := &throttlerTiming{ + t: t, + now: time.Now(), + } + th := newMemoryThrottlerForTest(t) + th.getNow = timing.getNow + th.doDelay = timing.doDelay + + ctx := context.Background() + + throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle1(ctx) + + timing.now = timing.now.Add(time.Millisecond) + throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 200 * time.Millisecond + throttle2(ctx) + + timing.now = timing.now.Add(time.Millisecond) + throttle3, err := th.CheckBruteforce(ctx, "192.168.0.2", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle3(ctx) + + timing.now = timing.now.Add(time.Millisecond) + throttle4, err := th.CheckBruteforce(ctx, "192.168.0.1", "action2") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle4(ctx) +} + +func TestThrottlerIPv6(t *testing.T) { + assert := assert.New(t) + timing := &throttlerTiming{ + t: t, + now: time.Now(), + } + th := newMemoryThrottlerForTest(t) + th.getNow = timing.getNow + th.doDelay = timing.doDelay + + ctx := context.Background() + + // Make sure full /64 subnets are throttled for IPv6. + throttle1, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::1", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle1(ctx) + + timing.now = timing.now.Add(time.Millisecond) + throttle2, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::2", "action1") + assert.NoError(err) + timing.expectedSleep = 200 * time.Millisecond + throttle2(ctx) + + // A diffent /64 subnet is not throttled yet. + timing.now = timing.now.Add(time.Millisecond) + throttle3, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0013::1", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle3(ctx) + + // A different action is not throttled. + timing.now = timing.now.Add(time.Millisecond) + throttle4, err := th.CheckBruteforce(ctx, "2001:db8:abcd:0012::1", "action2") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle4(ctx) +} + +func TestThrottler_Bruteforce(t *testing.T) { + assert := assert.New(t) + timing := &throttlerTiming{ + t: t, + now: time.Now(), + } + th := newMemoryThrottlerForTest(t) + th.getNow = timing.getNow + th.doDelay = timing.doDelay + + ctx := context.Background() + + for i := 0; i < maxBruteforceAttempts; i++ { + timing.now = timing.now.Add(time.Millisecond) + throttle, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + if i == 0 { + timing.expectedSleep = 100 * time.Millisecond + } else { + timing.expectedSleep *= 2 + if timing.expectedSleep > maxThrottleDelay { + timing.expectedSleep = maxThrottleDelay + } + } + throttle(ctx) + } + + timing.now = timing.now.Add(time.Millisecond) + _, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.ErrorIs(err, ErrBruteforceDetected) +} + +func TestThrottler_Cleanup(t *testing.T) { + assert := assert.New(t) + timing := &throttlerTiming{ + t: t, + now: time.Now(), + } + th := newMemoryThrottlerForTest(t) + th.getNow = timing.getNow + th.doDelay = timing.doDelay + + ctx := context.Background() + + throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle1(ctx) + + throttle2, err := th.CheckBruteforce(ctx, "192.168.0.2", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle2(ctx) + + timing.now = timing.now.Add(time.Hour) + throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action2") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle3(ctx) + + throttle4, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 200 * time.Millisecond + throttle4(ctx) + + timing.now = timing.now.Add(-time.Hour).Add(maxBruteforceAge).Add(time.Second) + th.cleanup(timing.now) + + assert.Len(th.getEntries("192.168.0.1", "action1"), 1) + assert.Len(th.getEntries("192.168.0.1", "action2"), 1) + + th.mu.RLock() + if _, found := th.clients["192.168.0.2"]; found { + assert.Fail("should have removed client \"192.168.0.2\"") + } + th.mu.RUnlock() + + throttle5, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 200 * time.Millisecond + throttle5(ctx) +} + +func TestThrottler_ExpirePartial(t *testing.T) { + assert := assert.New(t) + timing := &throttlerTiming{ + t: t, + now: time.Now(), + } + th := newMemoryThrottlerForTest(t) + th.getNow = timing.getNow + th.doDelay = timing.doDelay + + ctx := context.Background() + + throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle1(ctx) + + timing.now = timing.now.Add(time.Minute) + + throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 200 * time.Millisecond + throttle2(ctx) + + timing.now = timing.now.Add(maxBruteforceAge).Add(-time.Minute + time.Second) + + throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 200 * time.Millisecond + throttle3(ctx) +} + +func TestThrottler_ExpireAll(t *testing.T) { + assert := assert.New(t) + timing := &throttlerTiming{ + t: t, + now: time.Now(), + } + th := newMemoryThrottlerForTest(t) + th.getNow = timing.getNow + th.doDelay = timing.doDelay + + ctx := context.Background() + + throttle1, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle1(ctx) + + timing.now = timing.now.Add(time.Millisecond) + + throttle2, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 200 * time.Millisecond + throttle2(ctx) + + timing.now = timing.now.Add(maxBruteforceAge).Add(time.Second) + + throttle3, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + assert.NoError(err) + timing.expectedSleep = 100 * time.Millisecond + throttle3(ctx) +} + +func TestThrottler_Negative(t *testing.T) { + assert := assert.New(t) + timing := &throttlerTiming{ + t: t, + now: time.Now(), + } + th := newMemoryThrottlerForTest(t) + th.getNow = timing.getNow + th.doDelay = timing.doDelay + + ctx := context.Background() + + for i := 0; i < maxBruteforceAttempts*10; i++ { + timing.now = timing.now.Add(time.Millisecond) + throttle, err := th.CheckBruteforce(ctx, "192.168.0.1", "action1") + if err != nil { + assert.ErrorIs(err, ErrBruteforceDetected) + } + if i == 0 { + timing.expectedSleep = 100 * time.Millisecond + } else { + timing.expectedSleep *= 2 + if timing.expectedSleep > maxThrottleDelay { + timing.expectedSleep = maxThrottleDelay + } + } + throttle(ctx) + } +} diff --git a/internal/tools/tools.go b/tools.go similarity index 98% rename from internal/tools/tools.go rename to tools.go index 8ffb487..ec075a1 100644 --- a/internal/tools/tools.go +++ b/tools.go @@ -21,7 +21,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package tools +package signaling // Import applications that would otherwise not be detected by "go mod vendor". import ( diff --git a/transient_data.go b/transient_data.go new file mode 100644 index 0000000..120a454 --- /dev/null +++ b/transient_data.go @@ -0,0 +1,256 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2021 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "reflect" + "sync" + "time" +) + +type TransientListener interface { + SendMessage(message *ServerMessage) bool +} + +type TransientData struct { + mu sync.Mutex + data map[string]interface{} + listeners map[TransientListener]bool + timers map[string]*time.Timer + ttlCh chan<- struct{} +} + +// NewTransientData creates a new transient data container. +func NewTransientData() *TransientData { + return &TransientData{} +} + +func (t *TransientData) notifySet(key string, prev, value interface{}) { + msg := &ServerMessage{ + Type: "transient", + TransientData: &TransientDataServerMessage{ + Type: "set", + Key: key, + OldValue: prev, + Value: value, + }, + } + for listener := range t.listeners { + listener.SendMessage(msg) + } +} + +func (t *TransientData) notifyDeleted(key string, prev interface{}) { + msg := &ServerMessage{ + Type: "transient", + TransientData: &TransientDataServerMessage{ + Type: "remove", + Key: key, + OldValue: prev, + }, + } + for listener := range t.listeners { + listener.SendMessage(msg) + } +} + +// AddListener adds a new listener to be notified about changes. +func (t *TransientData) AddListener(listener TransientListener) { + t.mu.Lock() + defer t.mu.Unlock() + + if t.listeners == nil { + t.listeners = make(map[TransientListener]bool) + } + t.listeners[listener] = true + if len(t.data) > 0 { + msg := &ServerMessage{ + Type: "transient", + TransientData: &TransientDataServerMessage{ + Type: "initial", + Data: t.data, + }, + } + listener.SendMessage(msg) + } +} + +// RemoveListener removes a previously registered listener. +func (t *TransientData) RemoveListener(listener TransientListener) { + t.mu.Lock() + defer t.mu.Unlock() + + delete(t.listeners, listener) +} + +func (t *TransientData) updateTTL(key string, value interface{}, ttl time.Duration) { + if ttl <= 0 { + delete(t.timers, key) + } else { + t.removeAfterTTL(key, value, ttl) + } +} + +func (t *TransientData) removeAfterTTL(key string, value interface{}, ttl time.Duration) { + if ttl <= 0 { + return + } + + if old, found := t.timers[key]; found { + old.Stop() + } + + timer := time.AfterFunc(ttl, func() { + t.mu.Lock() + defer t.mu.Unlock() + + t.compareAndRemove(key, value) + if t.ttlCh != nil { + select { + case t.ttlCh <- struct{}{}: + default: + } + } + }) + if t.timers == nil { + t.timers = make(map[string]*time.Timer) + } + t.timers[key] = timer +} + +func (t *TransientData) doSet(key string, value interface{}, prev interface{}, ttl time.Duration) { + if t.data == nil { + t.data = make(map[string]interface{}) + } + t.data[key] = value + t.notifySet(key, prev, value) + t.removeAfterTTL(key, value, ttl) +} + +// Set sets a new value for the given key and notifies listeners +// if the value has been changed. +func (t *TransientData) Set(key string, value interface{}) bool { + return t.SetTTL(key, value, 0) +} + +// SetTTL sets a new value for the given key with a time-to-live and notifies +// listeners if the value has been changed. +func (t *TransientData) SetTTL(key string, value interface{}, ttl time.Duration) bool { + if value == nil { + return t.Remove(key) + } + + t.mu.Lock() + defer t.mu.Unlock() + + prev, found := t.data[key] + if found && reflect.DeepEqual(prev, value) { + t.updateTTL(key, value, ttl) + return false + } + + t.doSet(key, value, prev, ttl) + return true +} + +// CompareAndSet sets a new value for the given key only for a given old value +// and notifies listeners if the value has been changed. +func (t *TransientData) CompareAndSet(key string, old, value interface{}) bool { + return t.CompareAndSetTTL(key, old, value, 0) +} + +// CompareAndSetTTL sets a new value for the given key with a time-to-live, +// only for a given old value and notifies listeners if the value has been +// changed. +func (t *TransientData) CompareAndSetTTL(key string, old, value interface{}, ttl time.Duration) bool { + if value == nil { + return t.CompareAndRemove(key, old) + } + + t.mu.Lock() + defer t.mu.Unlock() + + prev, found := t.data[key] + if old != nil && (!found || !reflect.DeepEqual(prev, old)) { + return false + } else if old == nil && found { + return false + } + + t.doSet(key, value, prev, ttl) + return true +} + +func (t *TransientData) doRemove(key string, prev interface{}) { + delete(t.data, key) + if old, found := t.timers[key]; found { + old.Stop() + delete(t.timers, key) + } + t.notifyDeleted(key, prev) +} + +// Remove deletes the value with the given key and notifies listeners +// if the key was removed. +func (t *TransientData) Remove(key string) bool { + t.mu.Lock() + defer t.mu.Unlock() + + prev, found := t.data[key] + if !found { + return false + } + + t.doRemove(key, prev) + return true +} + +// CompareAndRemove deletes the value with the given key if it has a given value +// and notifies listeners if the key was removed. +func (t *TransientData) CompareAndRemove(key string, old interface{}) bool { + t.mu.Lock() + defer t.mu.Unlock() + + return t.compareAndRemove(key, old) +} + +func (t *TransientData) compareAndRemove(key string, old interface{}) bool { + prev, found := t.data[key] + if !found || !reflect.DeepEqual(prev, old) { + return false + } + + t.doRemove(key, prev) + return true +} + +// GetData returns a copy of the internal data. +func (t *TransientData) GetData() map[string]interface{} { + t.mu.Lock() + defer t.mu.Unlock() + + result := make(map[string]interface{}) + for k, v := range t.data { + result[k] = v + } + return result +} diff --git a/transient_data_test.go b/transient_data_test.go new file mode 100644 index 0000000..66ac3d6 --- /dev/null +++ b/transient_data_test.go @@ -0,0 +1,234 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2021 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func (t *TransientData) SetTTLChannel(ch chan<- struct{}) { + t.mu.Lock() + defer t.mu.Unlock() + + t.ttlCh = ch +} + +func Test_TransientData(t *testing.T) { + assert := assert.New(t) + data := NewTransientData() + assert.False(data.Set("foo", nil)) + assert.True(data.Set("foo", "bar")) + assert.False(data.Set("foo", "bar")) + assert.True(data.Set("foo", "baz")) + assert.False(data.CompareAndSet("foo", "bar", "lala")) + assert.True(data.CompareAndSet("foo", "baz", "lala")) + assert.False(data.CompareAndSet("test", nil, nil)) + assert.True(data.CompareAndSet("test", nil, "123")) + assert.False(data.CompareAndSet("test", nil, "456")) + assert.False(data.CompareAndRemove("test", "1234")) + assert.True(data.CompareAndRemove("test", "123")) + assert.False(data.Remove("lala")) + assert.True(data.Remove("foo")) + + ttlCh := make(chan struct{}) + data.SetTTLChannel(ttlCh) + assert.True(data.SetTTL("test", "1234", time.Millisecond)) + assert.Equal("1234", data.GetData()["test"]) + // Data is removed after the TTL + <-ttlCh + assert.Nil(data.GetData()["test"]) + + assert.True(data.SetTTL("test", "1234", time.Millisecond)) + assert.Equal("1234", data.GetData()["test"]) + assert.True(data.SetTTL("test", "2345", 3*time.Millisecond)) + assert.Equal("2345", data.GetData()["test"]) + // Data is removed after the TTL only if the value still matches + time.Sleep(2 * time.Millisecond) + assert.Equal("2345", data.GetData()["test"]) + // Data is removed after the (second) TTL + <-ttlCh + assert.Nil(data.GetData()["test"]) + + // Setting existing key will update the TTL + assert.True(data.SetTTL("test", "1234", time.Millisecond)) + assert.False(data.SetTTL("test", "1234", 3*time.Millisecond)) + // Data still exists after the first TTL + time.Sleep(2 * time.Millisecond) + assert.Equal("1234", data.GetData()["test"]) + // Data is removed after the (updated) TTL + <-ttlCh + assert.Nil(data.GetData()["test"]) +} + +func Test_TransientMessages(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + client1 := NewTestClient(t, server, hub) + defer client1.CloseWithBye() + require.NoError(client1.SendHello(testDefaultUserId + "1")) + hello1, err := client1.RunUntilHello(ctx) + require.NoError(err) + + require.NoError(client1.SetTransientData("foo", "bar", 0)) + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageError(msg, "not_in_room")) + } + + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + hello2, err := client2.RunUntilHello(ctx) + require.NoError(err) + + // Join room by id. + roomId := "test-room" + roomMsg, err := client1.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // Give message processing some time. + time.Sleep(10 * time.Millisecond) + + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2) + + session1 := hub.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession) + require.NotNil(session1, "Session %s does not exist", hello1.Hello.SessionId) + session2 := hub.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession) + require.NotNil(session2, "Session %s does not exist", hello2.Hello.SessionId) + + // Client 1 may modify transient data. + session1.SetPermissions([]Permission{PERMISSION_TRANSIENT_DATA}) + // Client 2 may not modify transient data. + session2.SetPermissions([]Permission{}) + + require.NoError(client2.SetTransientData("foo", "bar", 0)) + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageError(msg, "not_allowed")) + } + + require.NoError(client1.SetTransientData("foo", "bar", 0)) + + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageTransientSet(msg, "foo", "bar", nil)) + } + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageTransientSet(msg, "foo", "bar", nil)) + } + + require.NoError(client2.RemoveTransientData("foo")) + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageError(msg, "not_allowed")) + } + + // Setting the same value is ignored by the server. + require.NoError(client1.SetTransientData("foo", "bar", 0)) + ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel2() + + if msg, err := client1.RunUntilMessage(ctx2); err == nil { + assert.Fail("Expected no payload, got %+v", msg) + } else { + require.ErrorIs(err, context.DeadlineExceeded) + } + + data := map[string]interface{}{ + "hello": "world", + } + require.NoError(client1.SetTransientData("foo", data, 0)) + + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageTransientSet(msg, "foo", data, "bar")) + } + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageTransientSet(msg, "foo", data, "bar")) + } + + require.NoError(client1.RemoveTransientData("foo")) + + if msg, err := client1.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageTransientRemove(msg, "foo", data)) + } + if msg, err := client2.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageTransientRemove(msg, "foo", data)) + } + + // Removing a non-existing key is ignored by the server. + require.NoError(client1.RemoveTransientData("foo")) + ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel3() + + if msg, err := client1.RunUntilMessage(ctx3); err == nil { + assert.Fail("Expected no payload, got %+v", msg) + } else { + require.ErrorIs(err, context.DeadlineExceeded) + } + + require.NoError(client1.SetTransientData("abc", data, 10*time.Millisecond)) + + client3 := NewTestClient(t, server, hub) + defer client3.CloseWithBye() + require.NoError(client3.SendHello(testDefaultUserId + "3")) + hello3, err := client3.RunUntilHello(ctx) + require.NoError(err) + + roomMsg, err = client3.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + _, ignored, err := client3.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello, hello3.Hello) + require.NoError(err) + + var msg *ServerMessage + if len(ignored) == 0 { + msg, err = client3.RunUntilMessage(ctx) + require.NoError(err) + } else if len(ignored) == 1 { + msg = ignored[0] + } else { + require.Fail("Received too many messages: %+v", ignored) + } + + require.NoError(checkMessageTransientInitial(msg, map[string]interface{}{ + "abc": data, + })) + + time.Sleep(10 * time.Millisecond) + if msg, err = client3.RunUntilMessage(ctx); assert.NoError(err) { + require.NoError(checkMessageTransientRemove(msg, "abc", data)) + } +} diff --git a/internal/tools/vendor_helper_test.go b/vendor_helper_test.go similarity index 98% rename from internal/tools/vendor_helper_test.go rename to vendor_helper_test.go index 2cab8d3..8d73f5e 100644 --- a/internal/tools/vendor_helper_test.go +++ b/vendor_helper_test.go @@ -19,7 +19,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package tools +package signaling // Import modules that would otherwise not be detected by "go mod vendor". import ( diff --git a/server/virtualsession.go b/virtualsession.go similarity index 51% rename from server/virtualsession.go rename to virtualsession.go index bb484a9..cfbd435 100644 --- a/server/virtualsession.go +++ b/virtualsession.go @@ -19,23 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package server +package signaling import ( "context" "encoding/json" - "errors" + "log" "net/url" "sync/atomic" - "time" - - "github.com/strukturag/nextcloud-spreed-signaling/v2/api" - "github.com/strukturag/nextcloud-spreed-signaling/v2/async/events" - "github.com/strukturag/nextcloud-spreed-signaling/v2/internal" - "github.com/strukturag/nextcloud-spreed-signaling/v2/log" - "github.com/strukturag/nextcloud-spreed-signaling/v2/nats" - "github.com/strukturag/nextcloud-spreed-signaling/v2/session" - "github.com/strukturag/nextcloud-spreed-signaling/v2/talk" ) const ( @@ -45,53 +36,40 @@ const ( ) type VirtualSession struct { - logger log.Logger hub *Hub session *ClientSession - privateId api.PrivateSessionId - publicId api.PublicSessionId - data *session.SessionIdData - ctx context.Context - closeFunc context.CancelFunc + privateId string + publicId string + data *SessionIdData room atomic.Pointer[Room] - sessionId api.PublicSessionId + sessionId string userId string userData json.RawMessage - inCall internal.Flags - flags internal.Flags - options *api.AddSessionOptions + inCall Flags + flags Flags + options *AddSessionOptions - parseUserData func() (api.StringMap, error) - - asyncCh events.AsyncChannel + parseUserData func() (map[string]interface{}, error) } -func GetVirtualSessionId(session Session, sessionId api.PublicSessionId) api.PublicSessionId { +func GetVirtualSessionId(session Session, sessionId string) string { return session.PublicId() + "|" + sessionId } -func NewVirtualSession(session *ClientSession, privateId api.PrivateSessionId, publicId api.PublicSessionId, data *session.SessionIdData, msg *api.AddSessionInternalClientMessage) (*VirtualSession, error) { - ctx := log.NewLoggerContext(session.Context(), session.hub.logger) - ctx, closeFunc := context.WithCancel(ctx) - +func NewVirtualSession(session *ClientSession, privateId string, publicId string, data *SessionIdData, msg *AddSessionInternalClientMessage) (*VirtualSession, error) { result := &VirtualSession{ - logger: session.hub.logger, hub: session.hub, session: session, privateId: privateId, publicId: publicId, data: data, - ctx: ctx, - closeFunc: closeFunc, sessionId: msg.SessionId, userId: msg.UserId, userData: msg.User, parseUserData: parseUserData(msg.User), options: msg.Options, - - asyncCh: make(events.AsyncChannel, events.DefaultAsyncChannelSize), } if err := session.events.RegisterSessionListener(publicId, session.Backend(), result); err != nil { @@ -100,31 +78,30 @@ func NewVirtualSession(session *ClientSession, privateId api.PrivateSessionId, p if msg.InCall != nil { result.SetInCall(*msg.InCall) - } else if !session.HasFeature(api.ClientFeatureInternalInCall) { + } else if !session.HasFeature(ClientFeatureInternalInCall) { result.SetInCall(FlagInCall | FlagWithPhone) } if msg.Flags != 0 { result.SetFlags(msg.Flags) } - go result.run() return result, nil } func (s *VirtualSession) Context() context.Context { - return s.ctx + return s.session.Context() } -func (s *VirtualSession) PrivateId() api.PrivateSessionId { +func (s *VirtualSession) PrivateId() string { return s.privateId } -func (s *VirtualSession) PublicId() api.PublicSessionId { +func (s *VirtualSession) PublicId() string { return s.publicId } -func (s *VirtualSession) ClientType() api.ClientType { - return api.HelloClientTypeVirtual +func (s *VirtualSession) ClientType() string { + return HelloClientTypeVirtual } func (s *VirtualSession) GetInCall() int { @@ -139,11 +116,11 @@ func (s *VirtualSession) SetInCall(inCall int) bool { return s.inCall.Set(uint32(inCall)) } -func (s *VirtualSession) Data() *session.SessionIdData { +func (s *VirtualSession) Data() *SessionIdData { return s.data } -func (s *VirtualSession) Backend() *talk.Backend { +func (s *VirtualSession) Backend() *Backend { return s.session.Backend() } @@ -155,10 +132,6 @@ func (s *VirtualSession) ParsedBackendUrl() *url.URL { return s.session.ParsedBackendUrl() } -func (s *VirtualSession) ParsedBackendOcsUrl() *url.URL { - return s.session.ParsedBackendOcsUrl() -} - func (s *VirtualSession) UserId() string { return s.userId } @@ -167,15 +140,15 @@ func (s *VirtualSession) UserData() json.RawMessage { return s.userData } -func (s *VirtualSession) ParsedUserData() (api.StringMap, error) { +func (s *VirtualSession) ParsedUserData() (map[string]interface{}, error) { return s.parseUserData() } -func (s *VirtualSession) SetRoom(room *Room, joinTime time.Time) { +func (s *VirtualSession) SetRoom(room *Room) { s.room.Store(room) if room != nil { - if err := s.hub.roomSessions.SetRoomSession(s, api.RoomSessionId(s.PublicId())); err != nil { - s.logger.Printf("Error adding virtual room session %s: %s", s.PublicId(), err) + if err := s.hub.roomSessions.SetRoomSession(s, s.PublicId()); err != nil { + log.Printf("Error adding virtual room session %s: %s", s.PublicId(), err) } } else { s.hub.roomSessions.DeleteRoomSession(s) @@ -186,75 +159,49 @@ func (s *VirtualSession) GetRoom() *Room { return s.room.Load() } -func (s *VirtualSession) IsInRoom(id string) bool { - room := s.GetRoom() - return room != nil && room.Id() == id -} - func (s *VirtualSession) LeaveRoom(notify bool) *Room { room := s.GetRoom() if room == nil { return nil } - s.SetRoom(nil, time.Time{}) + s.SetRoom(nil) room.RemoveSession(s) return room } -func (s *VirtualSession) AsyncChannel() events.AsyncChannel { - return s.asyncCh -} - -func (s *VirtualSession) run() { - for { - select { - case <-s.ctx.Done(): - return - case msg := <-s.asyncCh: - s.processAsyncNatsMessage(msg) - for count := len(s.asyncCh); count > 0; count-- { - s.processAsyncNatsMessage(<-s.asyncCh) - } - } - } -} - func (s *VirtualSession) Close() { s.CloseWithFeedback(nil, nil) } -func (s *VirtualSession) CloseWithFeedback(session Session, message *api.ClientMessage) { - s.closeFunc() - +func (s *VirtualSession) CloseWithFeedback(session Session, message *ClientMessage) { room := s.GetRoom() s.session.RemoveVirtualSession(s) removed := s.session.hub.removeSession(s) if removed && room != nil { go s.notifyBackendRemoved(room, session, message) } - if err := s.session.events.UnregisterSessionListener(s.PublicId(), s.session.Backend(), s); err != nil && !errors.Is(err, nats.ErrConnectionClosed) { - s.logger.Printf("Error unsubscribing listener for session %s: %s", s.publicId, err) - } + s.session.events.UnregisterSessionListener(s.PublicId(), s.session.Backend(), s) } -func (s *VirtualSession) notifyBackendRemoved(room *Room, session Session, message *api.ClientMessage) { - ctx := log.NewLoggerContext(context.Background(), s.logger) - ctx, cancel := context.WithTimeout(ctx, s.hub.backendTimeout) +func (s *VirtualSession) notifyBackendRemoved(room *Room, session Session, message *ClientMessage) { + ctx, cancel := context.WithTimeout(context.Background(), s.hub.backendTimeout) defer cancel() - if options := s.Options(); options != nil && options.ActorId != "" && options.ActorType != "" { - request := talk.NewBackendClientRoomRequest(room.Id(), s.UserId(), api.RoomSessionId(s.PublicId())) + if options := s.Options(); options != nil { + request := NewBackendClientRoomRequest(room.Id(), s.UserId(), s.PublicId()) request.Room.Action = "leave" - request.Room.ActorId = options.ActorId - request.Room.ActorType = options.ActorType + if options != nil { + request.Room.ActorId = options.ActorId + request.Room.ActorType = options.ActorType + } - var response talk.BackendClientResponse - if err := s.hub.backend.PerformJSONRequest(ctx, s.ParsedBackendOcsUrl(), request, &response); err != nil { + var response BackendClientResponse + if err := s.hub.backend.PerformJSONRequest(ctx, s.ParsedBackendUrl(), request, &response); err != nil { virtualSessionId := GetVirtualSessionId(s.session, s.PublicId()) - s.logger.Printf("Could not leave virtual session %s at backend %s: %s", virtualSessionId, s.BackendUrl(), err) + log.Printf("Could not leave virtual session %s at backend %s: %s", virtualSessionId, s.BackendUrl(), err) if session != nil && message != nil { - reply := message.NewErrorServerMessage(api.NewError("remove_failed", "Could not remove virtual session from backend.")) + reply := message.NewErrorServerMessage(NewError("remove_failed", "Could not remove virtual session from backend.")) session.SendMessage(reply) } return @@ -263,30 +210,30 @@ func (s *VirtualSession) notifyBackendRemoved(room *Room, session Session, messa if response.Type == "error" { virtualSessionId := GetVirtualSessionId(s.session, s.PublicId()) if session != nil && message != nil && (response.Error == nil || response.Error.Code != "no_such_room") { - s.logger.Printf("Could not leave virtual session %s at backend %s: %+v", virtualSessionId, s.BackendUrl(), response.Error) - reply := message.NewErrorServerMessage(api.NewError("remove_failed", response.Error.Error())) + log.Printf("Could not leave virtual session %s at backend %s: %+v", virtualSessionId, s.BackendUrl(), response.Error) + reply := message.NewErrorServerMessage(NewError("remove_failed", response.Error.Error())) session.SendMessage(reply) } return } } else { - request := talk.NewBackendClientSessionRequest(room.Id(), "remove", s.PublicId(), &api.AddSessionInternalClientMessage{ + request := NewBackendClientSessionRequest(room.Id(), "remove", s.PublicId(), &AddSessionInternalClientMessage{ UserId: s.userId, User: s.userData, }) - var response talk.BackendClientSessionResponse - err := s.hub.backend.PerformJSONRequest(ctx, s.ParsedBackendOcsUrl(), request, &response) + var response BackendClientSessionResponse + err := s.hub.backend.PerformJSONRequest(ctx, s.ParsedBackendUrl(), request, &response) if err != nil { - s.logger.Printf("Could not remove virtual session %s from backend %s: %s", s.PublicId(), s.BackendUrl(), err) + log.Printf("Could not remove virtual session %s from backend %s: %s", s.PublicId(), s.BackendUrl(), err) if session != nil && message != nil { - reply := message.NewErrorServerMessage(api.NewError("remove_failed", "Could not remove virtual session from backend.")) + reply := message.NewErrorServerMessage(NewError("remove_failed", "Could not remove virtual session from backend.")) session.SendMessage(reply) } } } } -func (s *VirtualSession) HasPermission(permission api.Permission) bool { +func (s *VirtualSession) HasPermission(permission Permission) bool { return true } @@ -294,7 +241,7 @@ func (s *VirtualSession) Session() *ClientSession { return s.session } -func (s *VirtualSession) SessionId() api.PublicSessionId { +func (s *VirtualSession) SessionId() string { return s.sessionId } @@ -314,21 +261,11 @@ func (s *VirtualSession) Flags() uint32 { return s.flags.Get() } -func (s *VirtualSession) Options() *api.AddSessionOptions { +func (s *VirtualSession) Options() *AddSessionOptions { return s.options } -func (s *VirtualSession) processAsyncNatsMessage(msg *nats.Msg) { - var message events.AsyncMessage - if err := nats.Decode(msg, &message); err != nil { - s.logger.Printf("Could not decode NATS message %+v: %s", msg, err) - return - } - - s.processAsyncMessage(&message) -} - -func (s *VirtualSession) processAsyncMessage(message *events.AsyncMessage) { +func (s *VirtualSession) ProcessAsyncSessionMessage(message *AsyncMessage) { if message.Type == "message" && message.Message != nil { switch message.Message.Type { case "message": @@ -337,12 +274,12 @@ func (s *VirtualSession) processAsyncMessage(message *events.AsyncMessage) { message.Message.Message.Recipient.Type == "session" && message.Message.Message.Recipient.SessionId == s.PublicId() { // The client should see his session id as recipient. - message.Message.Message.Recipient = &api.MessageClientMessageRecipient{ + message.Message.Message.Recipient = &MessageClientMessageRecipient{ Type: "session", SessionId: s.SessionId(), UserId: s.UserId(), } - s.session.processAsyncMessage(message) + s.session.ProcessAsyncSessionMessage(message) } case "event": if room := s.GetRoom(); room != nil && @@ -350,8 +287,8 @@ func (s *VirtualSession) processAsyncMessage(message *events.AsyncMessage) { message.Message.Event.Type == "disinvite" && message.Message.Event.Disinvite != nil && message.Message.Event.Disinvite.RoomId == room.Id() { - s.logger.Printf("Virtual session %s was disinvited from room %s, hanging up", s.PublicId(), room.Id()) - payload := api.StringMap{ + log.Printf("Virtual session %s was disinvited from room %s, hanging up", s.PublicId(), room.Id()) + payload := map[string]interface{}{ "type": "hangup", "hangup": map[string]string{ "reason": "disinvited", @@ -359,17 +296,17 @@ func (s *VirtualSession) processAsyncMessage(message *events.AsyncMessage) { } data, err := json.Marshal(payload) if err != nil { - s.logger.Printf("could not marshal control payload %+v: %s", payload, err) + log.Printf("could not marshal control payload %+v: %s", payload, err) return } - s.session.processAsyncMessage(&events.AsyncMessage{ + s.session.ProcessAsyncSessionMessage(&AsyncMessage{ Type: "message", SendTime: message.SendTime, - Message: &api.ServerMessage{ + Message: &ServerMessage{ Type: "control", - Control: &api.ControlServerMessage{ - Recipient: &api.MessageClientMessageRecipient{ + Control: &ControlServerMessage{ + Recipient: &MessageClientMessageRecipient{ Type: "session", SessionId: s.SessionId(), UserId: s.UserId(), @@ -385,21 +322,21 @@ func (s *VirtualSession) processAsyncMessage(message *events.AsyncMessage) { message.Message.Control.Recipient.Type == "session" && message.Message.Control.Recipient.SessionId == s.PublicId() { // The client should see his session id as recipient. - message.Message.Control.Recipient = &api.MessageClientMessageRecipient{ + message.Message.Control.Recipient = &MessageClientMessageRecipient{ Type: "session", SessionId: s.SessionId(), UserId: s.UserId(), } - s.session.processAsyncMessage(message) + s.session.ProcessAsyncSessionMessage(message) } } } } -func (s *VirtualSession) SendError(e *api.Error) bool { +func (s *VirtualSession) SendError(e *Error) bool { return s.session.SendError(e) } -func (s *VirtualSession) SendMessage(message *api.ServerMessage) bool { +func (s *VirtualSession) SendMessage(message *ServerMessage) bool { return s.session.SendMessage(message) } diff --git a/virtualsession_test.go b/virtualsession_test.go new file mode 100644 index 0000000..bc9ca10 --- /dev/null +++ b/virtualsession_test.go @@ -0,0 +1,518 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2019 struktur AG + * + * @author Joachim Bauch + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package signaling + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVirtualSession(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + roomId := "the-room-id" + emptyProperties := json.RawMessage("{}") + backend := &Backend{ + id: "compat", + compat: true, + } + room, err := hub.createRoom(roomId, emptyProperties, backend) + require.NoError(err) + defer room.Close() + + clientInternal := NewTestClient(t, server, hub) + defer clientInternal.CloseWithBye() + require.NoError(clientInternal.SendHelloInternal()) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + require.NoError(client.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + if hello, err := clientInternal.RunUntilHello(ctx); assert.NoError(err) { + assert.Empty(hello.Hello.UserId) + assert.NotEmpty(hello.Hello.SessionId) + assert.NotEmpty(hello.Hello.ResumeId) + } + hello, err := client.RunUntilHello(ctx) + assert.NoError(err) + + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // Ignore "join" events. + assert.NoError(client.DrainMessages(ctx)) + + internalSessionId := "session1" + userId := "user1" + msgAdd := &ClientMessage{ + Type: "internal", + Internal: &InternalClientMessage{ + Type: "addsession", + AddSession: &AddSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ + SessionId: internalSessionId, + RoomId: roomId, + }, + UserId: userId, + Flags: FLAG_MUTED_SPEAKING, + }, + }, + } + require.NoError(clientInternal.WriteJSON(msgAdd)) + + msg1, err := client.RunUntilMessage(ctx) + require.NoError(err) + // The public session id will be generated by the server, so don't check for it. + require.NoError(client.checkMessageJoinedSession(msg1, "", userId)) + sessionId := msg1.Event.Join[0].SessionId + session := hub.GetSessionByPublicId(sessionId) + if assert.NotNil(session, "Could not get virtual session %s", sessionId) { + assert.Equal(HelloClientTypeVirtual, session.ClientType()) + sid := session.(*VirtualSession).SessionId() + assert.Equal(internalSessionId, sid) + } + + // Also a participants update event will be triggered for the virtual user. + msg2, err := client.RunUntilMessage(ctx) + require.NoError(err) + if updateMsg, err := checkMessageParticipantsInCall(msg2); assert.NoError(err) { + assert.Equal(roomId, updateMsg.RoomId) + if assert.Len(updateMsg.Users, 1) { + assert.Equal(sessionId, updateMsg.Users[0]["sessionId"]) + assert.Equal(true, updateMsg.Users[0]["virtual"]) + assert.EqualValues((FlagInCall | FlagWithPhone), updateMsg.Users[0]["inCall"]) + } + } + + msg3, err := client.RunUntilMessage(ctx) + require.NoError(err) + + if flagsMsg, err := checkMessageParticipantFlags(msg3); assert.NoError(err) { + assert.Equal(roomId, flagsMsg.RoomId) + assert.Equal(sessionId, flagsMsg.SessionId) + assert.EqualValues(FLAG_MUTED_SPEAKING, flagsMsg.Flags) + } + + newFlags := uint32(FLAG_TALKING) + msgFlags := &ClientMessage{ + Type: "internal", + Internal: &InternalClientMessage{ + Type: "updatesession", + UpdateSession: &UpdateSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ + SessionId: internalSessionId, + RoomId: roomId, + }, + Flags: &newFlags, + }, + }, + } + require.NoError(clientInternal.WriteJSON(msgFlags)) + + msg4, err := client.RunUntilMessage(ctx) + require.NoError(err) + + if flagsMsg, err := checkMessageParticipantFlags(msg4); assert.NoError(err) { + assert.Equal(roomId, flagsMsg.RoomId) + assert.Equal(sessionId, flagsMsg.SessionId) + assert.EqualValues(newFlags, flagsMsg.Flags) + } + + // A new client will receive the initial flags of the virtual session. + client2 := NewTestClient(t, server, hub) + defer client2.CloseWithBye() + require.NoError(client2.SendHello(testDefaultUserId + "2")) + + _, err = client2.RunUntilHello(ctx) + require.NoError(err) + + roomMsg, err = client2.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + gotFlags := false + var receivedMessages []*ServerMessage + for !gotFlags { + messages, err := client2.GetPendingMessages(ctx) + if err != nil { + assert.NoError(err) + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + break + } + } + + receivedMessages = append(receivedMessages, messages...) + for _, msg := range messages { + if msg.Type != "event" || msg.Event.Target != "participants" || msg.Event.Type != "flags" { + continue + } + + if assert.Equal(roomId, msg.Event.Flags.RoomId) && + assert.Equal(sessionId, msg.Event.Flags.SessionId) && + assert.EqualValues(newFlags, msg.Event.Flags.Flags) { + gotFlags = true + break + } + } + } + assert.True(gotFlags, "Didn't receive initial flags in %+v", receivedMessages) + + // Ignore "join" messages from second client + assert.NoError(client.DrainMessages(ctx)) + + // When sending to a virtual session, the message is sent to the actual + // client and contains a "Recipient" block with the internal session id. + recipient := MessageClientMessageRecipient{ + Type: "session", + SessionId: sessionId, + } + + data := "from-client-to-virtual" + require.NoError(client.SendMessage(recipient, data)) + + msg2, err = clientInternal.RunUntilMessage(ctx) + require.NoError(err) + require.NoError(checkMessageType(msg2, "message")) + require.NoError(checkMessageSender(hub, msg2.Message.Sender, "session", hello.Hello)) + + if assert.NotNil(msg2.Message.Recipient) { + assert.Equal("session", msg2.Message.Recipient.Type) + assert.Equal(internalSessionId, msg2.Message.Recipient.SessionId) + } + + var payload string + if err := json.Unmarshal(msg2.Message.Data, &payload); assert.NoError(err) { + assert.Equal(data, payload) + } + + msgRemove := &ClientMessage{ + Type: "internal", + Internal: &InternalClientMessage{ + Type: "removesession", + RemoveSession: &RemoveSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ + SessionId: internalSessionId, + RoomId: roomId, + }, + }, + }, + } + require.NoError(clientInternal.WriteJSON(msgRemove)) + + if msg5, err := client.RunUntilMessage(ctx); assert.NoError(err) { + assert.NoError(client.checkMessageRoomLeaveSession(msg5, sessionId)) + } +} + +func checkHasEntryWithInCall(message *RoomEventServerMessage, sessionId string, entryType string, inCall int) error { + found := false + for _, entry := range message.Users { + if sid, ok := entry["sessionId"].(string); ok && sid == sessionId { + if value, ok := entry[entryType].(bool); !ok || !value { + return fmt.Errorf("Expected %s user, got %+v", entryType, entry) + } + + if value, ok := entry["inCall"].(float64); !ok || int(value) != inCall { + return fmt.Errorf("Expected in call %d, got %+v", inCall, entry) + } + found = true + break + } + } + + if !found { + return fmt.Errorf("No user with session id %s found, got %+v", sessionId, message) + } + + return nil +} + +func TestVirtualSessionCustomInCall(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + roomId := "the-room-id" + emptyProperties := json.RawMessage("{}") + backend := &Backend{ + id: "compat", + compat: true, + } + room, err := hub.createRoom(roomId, emptyProperties, backend) + require.NoError(err) + defer room.Close() + + clientInternal := NewTestClient(t, server, hub) + defer clientInternal.CloseWithBye() + features := []string{ + ClientFeatureInternalInCall, + } + require.NoError(clientInternal.SendHelloInternalWithFeatures(features)) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + require.NoError(client.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + helloInternal, err := clientInternal.RunUntilHello(ctx) + if assert.NoError(err) { + assert.Empty(helloInternal.Hello.UserId) + assert.NotEmpty(helloInternal.Hello.SessionId) + assert.NotEmpty(helloInternal.Hello.ResumeId) + } + roomMsg, err := clientInternal.JoinRoomWithRoomSession(ctx, roomId, "") + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + hello, err := client.RunUntilHello(ctx) + assert.NoError(err) + roomMsg, err = client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + if _, additional, err := clientInternal.RunUntilJoinedAndReturn(ctx, helloInternal.Hello, hello.Hello); assert.NoError(err) { + if assert.Len(additional, 1) && assert.Equal("event", additional[0].Type) { + assert.Equal("participants", additional[0].Event.Target) + assert.Equal("update", additional[0].Event.Type) + assert.Equal(helloInternal.Hello.SessionId, additional[0].Event.Update.Users[0]["sessionId"]) + assert.EqualValues(0, additional[0].Event.Update.Users[0]["inCall"]) + } + } + assert.NoError(client.RunUntilJoined(ctx, helloInternal.Hello, hello.Hello)) + + internalSessionId := "session1" + userId := "user1" + msgAdd := &ClientMessage{ + Type: "internal", + Internal: &InternalClientMessage{ + Type: "addsession", + AddSession: &AddSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ + SessionId: internalSessionId, + RoomId: roomId, + }, + UserId: userId, + Flags: FLAG_MUTED_SPEAKING, + }, + }, + } + require.NoError(clientInternal.WriteJSON(msgAdd)) + + msg1, err := client.RunUntilMessage(ctx) + require.NoError(err) + // The public session id will be generated by the server, so don't check for it. + require.NoError(client.checkMessageJoinedSession(msg1, "", userId)) + sessionId := msg1.Event.Join[0].SessionId + session := hub.GetSessionByPublicId(sessionId) + if assert.NotNil(session) { + assert.Equal(HelloClientTypeVirtual, session.ClientType()) + sid := session.(*VirtualSession).SessionId() + assert.Equal(internalSessionId, sid) + } + + // Also a participants update event will be triggered for the virtual user. + msg2, err := client.RunUntilMessage(ctx) + require.NoError(err) + if updateMsg, err := checkMessageParticipantsInCall(msg2); assert.NoError(err) { + assert.Equal(roomId, updateMsg.RoomId) + assert.Len(updateMsg.Users, 2) + + assert.NoError(checkHasEntryWithInCall(updateMsg, sessionId, "virtual", 0)) + assert.NoError(checkHasEntryWithInCall(updateMsg, helloInternal.Hello.SessionId, "internal", 0)) + } + + msg3, err := client.RunUntilMessage(ctx) + require.NoError(err) + + if flagsMsg, err := checkMessageParticipantFlags(msg3); assert.NoError(err) { + assert.Equal(roomId, flagsMsg.RoomId) + assert.Equal(sessionId, flagsMsg.SessionId) + assert.EqualValues(FLAG_MUTED_SPEAKING, flagsMsg.Flags) + } + + // The internal session can change its "inCall" flags + msgInCall := &ClientMessage{ + Type: "internal", + Internal: &InternalClientMessage{ + Type: "incall", + InCall: &InCallInternalClientMessage{ + InCall: FlagInCall | FlagWithAudio, + }, + }, + } + require.NoError(clientInternal.WriteJSON(msgInCall)) + + msg4, err := client.RunUntilMessage(ctx) + require.NoError(err) + if updateMsg, err := checkMessageParticipantsInCall(msg4); assert.NoError(err) { + assert.Equal(roomId, updateMsg.RoomId) + assert.Len(updateMsg.Users, 2) + assert.NoError(checkHasEntryWithInCall(updateMsg, sessionId, "virtual", 0)) + assert.NoError(checkHasEntryWithInCall(updateMsg, helloInternal.Hello.SessionId, "internal", FlagInCall|FlagWithAudio)) + } + + // The internal session can change the "inCall" flags of a virtual session + newInCall := FlagInCall | FlagWithPhone + msgInCall2 := &ClientMessage{ + Type: "internal", + Internal: &InternalClientMessage{ + Type: "updatesession", + UpdateSession: &UpdateSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ + SessionId: internalSessionId, + RoomId: roomId, + }, + InCall: &newInCall, + }, + }, + } + require.NoError(clientInternal.WriteJSON(msgInCall2)) + + msg5, err := client.RunUntilMessage(ctx) + require.NoError(err) + if updateMsg, err := checkMessageParticipantsInCall(msg5); assert.NoError(err) { + assert.Equal(roomId, updateMsg.RoomId) + assert.Len(updateMsg.Users, 2) + assert.NoError(checkHasEntryWithInCall(updateMsg, sessionId, "virtual", newInCall)) + assert.NoError(checkHasEntryWithInCall(updateMsg, helloInternal.Hello.SessionId, "internal", FlagInCall|FlagWithAudio)) + } +} + +func TestVirtualSessionCleanup(t *testing.T) { + t.Parallel() + CatchLogForTest(t) + require := require.New(t) + assert := assert.New(t) + hub, _, _, server := CreateHubForTest(t) + + roomId := "the-room-id" + emptyProperties := json.RawMessage("{}") + backend := &Backend{ + id: "compat", + compat: true, + } + room, err := hub.createRoom(roomId, emptyProperties, backend) + require.NoError(err) + defer room.Close() + + clientInternal := NewTestClient(t, server, hub) + defer clientInternal.CloseWithBye() + require.NoError(clientInternal.SendHelloInternal()) + + client := NewTestClient(t, server, hub) + defer client.CloseWithBye() + require.NoError(client.SendHello(testDefaultUserId)) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + if hello, err := clientInternal.RunUntilHello(ctx); assert.NoError(err) { + assert.Empty(hello.Hello.UserId) + assert.NotEmpty(hello.Hello.SessionId) + assert.NotEmpty(hello.Hello.ResumeId) + } + _, err = client.RunUntilHello(ctx) + assert.NoError(err) + + roomMsg, err := client.JoinRoom(ctx, roomId) + require.NoError(err) + require.Equal(roomId, roomMsg.Room.RoomId) + + // Ignore "join" events. + assert.NoError(client.DrainMessages(ctx)) + + internalSessionId := "session1" + userId := "user1" + msgAdd := &ClientMessage{ + Type: "internal", + Internal: &InternalClientMessage{ + Type: "addsession", + AddSession: &AddSessionInternalClientMessage{ + CommonSessionInternalClientMessage: CommonSessionInternalClientMessage{ + SessionId: internalSessionId, + RoomId: roomId, + }, + UserId: userId, + Flags: FLAG_MUTED_SPEAKING, + }, + }, + } + require.NoError(clientInternal.WriteJSON(msgAdd)) + + msg1, err := client.RunUntilMessage(ctx) + require.NoError(err) + // The public session id will be generated by the server, so don't check for it. + require.NoError(client.checkMessageJoinedSession(msg1, "", userId)) + sessionId := msg1.Event.Join[0].SessionId + session := hub.GetSessionByPublicId(sessionId) + if assert.NotNil(session) { + assert.Equal(HelloClientTypeVirtual, session.ClientType()) + sid := session.(*VirtualSession).SessionId() + assert.Equal(internalSessionId, sid) + } + + // Also a participants update event will be triggered for the virtual user. + msg2, err := client.RunUntilMessage(ctx) + require.NoError(err) + if updateMsg, err := checkMessageParticipantsInCall(msg2); assert.NoError(err) { + assert.Equal(roomId, updateMsg.RoomId) + if assert.Len(updateMsg.Users, 1) { + assert.Equal(sessionId, updateMsg.Users[0]["sessionId"]) + assert.Equal(true, updateMsg.Users[0]["virtual"]) + assert.EqualValues((FlagInCall | FlagWithPhone), updateMsg.Users[0]["inCall"]) + } + } + + msg3, err := client.RunUntilMessage(ctx) + require.NoError(err) + + if flagsMsg, err := checkMessageParticipantFlags(msg3); assert.NoError(err) { + assert.Equal(roomId, flagsMsg.RoomId) + assert.Equal(sessionId, flagsMsg.SessionId) + assert.EqualValues(FLAG_MUTED_SPEAKING, flagsMsg.Flags) + } + + // The virtual sessions are closed when the parent session is deleted. + clientInternal.CloseWithBye() + + msg2, err = client.RunUntilMessage(ctx) + require.NoError(err) + assert.NoError(client.checkMessageRoomLeaveSession(msg2, sessionId)) +}