Repository: mattermost/mattermost-push-proxy Branch: master Commit: 7852e23b8f77 Files: 36 Total size: 184.7 KB Directory structure: gitextract_wfqcrn85/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── NOTICE.txt ├── README.md ├── bin/ │ └── .gitignore ├── build/ │ └── setup.txt ├── config/ │ └── mattermost-push-proxy.sample.json ├── docker/ │ ├── Dockerfile │ ├── Dockerfile.fips │ └── entrypoint ├── go.mod ├── go.sum ├── internal/ │ └── version/ │ └── version.go ├── main.go ├── package.json ├── scripts/ │ └── go_install.sh ├── server/ │ ├── android_notification_server.go │ ├── android_notification_test.go │ ├── apple_notification_server.go │ ├── config_push_proxy.go │ ├── logger.go │ ├── logger_test.go │ ├── metrics.go │ ├── metrics_test.go │ ├── push_notification.go │ ├── push_response.go │ ├── server.go │ └── server_test.go └── swagger/ ├── spec.yaml └── template.hbs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" reviewers: - "mattermost/cloud-sre" open-pull-requests-limit: 5 groups: Github Actions updates: applies-to: version-updates dependency-type: production schedule: # Check for updates to GitHub Actions every week day: "monday" time: "10:00" interval: "weekly" ================================================ FILE: .github/workflows/ci.yml ================================================ name: Build and Test on: push: branches: - master pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: contents: read id-token: write # Required for OIDC authentication with Chainguard identity env: GO_VERSION: 1.24.6 FIPS_ENABLED: true jobs: lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Lint run: make lint test: name: Test runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Test run: make test package: name: Package runs-on: ubuntu-latest needs: [lint, test] steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: build-package/package run: make package build-amd64: name: Build AMD64 runs-on: ubuntu-latest needs: [lint, test] if: github.actor != 'dependabot[bot]' timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Build Docker Image for AMD64 run: make build-image-amd64-with-tags build-arm64: name: Build ARM64 runs-on: ubuntu-24.04-arm needs: [lint, test] if: github.actor != 'dependabot[bot]' timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Build Docker Image for ARM64 run: make build-image-arm64-with-tags fips-build-amd64: name: FIPS Build AMD64 runs-on: ubuntu-latest needs: [lint, test] if: github.actor != 'dependabot[bot]' timeout-minutes: 30 permissions: contents: read id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup Chainguard uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0 with: identity: ${{ secrets.CHAINGUARD_IDENTITY }} - name: Build FIPS Docker Image for AMD64 run: make build-image-fips-amd64-with-tags fips-build-arm64: name: FIPS Build ARM64 runs-on: ubuntu-24.04-arm needs: [lint, test] if: github.actor != 'dependabot[bot]' timeout-minutes: 30 permissions: contents: read id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup Chainguard uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0 with: identity: ${{ secrets.CHAINGUARD_IDENTITY }} - name: Build FIPS Docker Image for ARM64 run: make build-image-fips-arm64-with-tags fips-security-scan: name: FIPS Security Scan runs-on: ubuntu-latest needs: [lint, test] if: github.actor != 'dependabot[bot]' steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup Chainguard uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0 with: identity: ${{ secrets.CHAINGUARD_IDENTITY }} - name: Build FIPS Docker image for scanning run: make build-image-fips-amd64-with-tags - name: Run Grype vulnerability scanner uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2 with: image: "mattermost/mattermost-push-proxy-fips:${{ github.ref == 'refs/heads/master' && 'master' || format('dev-{0}', github.sha) }}" output-format: table fail-build: false security-scan: name: Security Scan runs-on: ubuntu-latest needs: [lint, test] if: github.actor != 'dependabot[bot]' steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Build Docker image for scanning run: make build-image-amd64-with-tags - name: Run Grype vulnerability scanner uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2 with: image: "mattermost-push-proxy:${{ github.ref == 'refs/heads/master' && 'master' || format('dev-{0}', github.sha) }}" output-format: table fail-build: false pr-deploy-amd64: name: PR Deploy AMD64 runs-on: ubuntu-latest needs: [security-scan, build-amd64] if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]' permissions: contents: read id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push AMD64 PR images run: | # Build with default APP_NAME (avoids filesystem issues) make build-image-amd64-with-tags # Retag with correct namespace for pushing docker tag mattermost-push-proxy:dev-${{ github.sha }}-amd64 mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64 docker tag mattermost-push-proxy:dev-${{ github.sha }} mattermost/mattermost-push-proxy:dev-${{ github.sha }} # Push to correct namespace docker push mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64 echo "✅ AMD64 image pushed: mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64" pr-deploy-arm64: name: PR Deploy ARM64 runs-on: ubuntu-24.04-arm needs: [security-scan, build-arm64] if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]' permissions: contents: read id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push ARM64 PR images run: | # Build with default APP_NAME (avoids filesystem issues) make build-image-arm64-with-tags # Retag with correct namespace for pushing docker tag mattermost-push-proxy:dev-${{ github.sha }}-arm64 mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64 docker tag mattermost-push-proxy:dev-${{ github.sha }} mattermost/mattermost-push-proxy:dev-${{ github.sha }} # Push to correct namespace docker push mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64 echo "✅ ARM64 image pushed: mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64" pr-deploy-manifest: name: PR Deploy Manifest runs-on: ubuntu-latest needs: [pr-deploy-amd64, pr-deploy-arm64] if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]' permissions: contents: read id-token: write steps: - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create and push multi-arch manifest run: | # Create multi-platform manifest for PR testing docker manifest create mattermost/mattermost-push-proxy:dev-${{ github.sha }} \ --amend mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64 \ --amend mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64 docker manifest push mattermost/mattermost-push-proxy:dev-${{ github.sha }} # Clean up intermediate architecture-specific tags (like production) echo "Cleaning up intermediate architecture-specific tags..." docker rmi mattermost/mattermost-push-proxy:dev-${{ github.sha }}-amd64 2>/dev/null || true docker rmi mattermost/mattermost-push-proxy:dev-${{ github.sha }}-arm64 2>/dev/null || true echo "✅ Multi-arch PR image available (arch-specific tags removed):" echo " docker pull mattermost/mattermost-push-proxy:dev-${{ github.sha }}" pr-deploy-fips-amd64: name: PR Deploy FIPS AMD64 runs-on: ubuntu-latest needs: [fips-security-scan, fips-build-amd64] if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]' permissions: contents: read id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup Chainguard Identity uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0 with: identity: ${{ secrets.CHAINGUARD_IDENTITY }} - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push FIPS AMD64 PR images run: | # Build with default APP_NAME (avoids filesystem issues) make build-image-fips-amd64-with-tags # Retag with correct namespace for pushing docker tag mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64 mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64 docker tag mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} # Push to correct namespace docker push mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64 echo "✅ FIPS AMD64 image pushed: mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64" pr-deploy-fips-arm64: name: PR Deploy FIPS ARM64 runs-on: ubuntu-24.04-arm needs: [fips-security-scan, fips-build-arm64] if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]' permissions: contents: read id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup Chainguard Identity uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0 with: identity: ${{ secrets.CHAINGUARD_IDENTITY }} - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push FIPS ARM64 PR images run: | # Build with default APP_NAME (avoids filesystem issues) make build-image-fips-arm64-with-tags # Retag with correct namespace for pushing docker tag mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64 mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64 docker tag mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} # Push to correct namespace docker push mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64 echo "✅ FIPS ARM64 image pushed: mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64" pr-deploy-fips-manifest: name: PR Deploy FIPS Manifest runs-on: ubuntu-latest needs: [pr-deploy-fips-amd64, pr-deploy-fips-arm64] if: github.event_name == 'pull_request' && github.repository_owner == 'mattermost' && github.actor != 'dependabot[bot]' permissions: contents: read id-token: write steps: - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create and push FIPS multi-arch manifest run: | # Create multi-platform FIPS manifest for PR testing docker manifest create mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} \ --amend mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64 \ --amend mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64 docker manifest push mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }} # Clean up intermediate FIPS architecture-specific tags (like production) echo "Cleaning up intermediate FIPS architecture-specific tags..." docker rmi mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-amd64 2>/dev/null || true docker rmi mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}-arm64 2>/dev/null || true echo "✅ Multi-arch FIPS PR image available (arch-specific tags removed):" echo " docker pull mattermost/mattermost-push-proxy-fips:dev-${{ github.sha }}" deploy-amd64: name: Deploy AMD64 runs-on: ubuntu-latest needs: [security-scan, build-amd64] if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]' permissions: contents: write id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push AMD64 image run: | # Build AMD64 image with temp tag for cleanup SHORT_SHA=${GITHUB_SHA:0:7} make build-image-amd64-with-tags # Retag with temp namespace for later cleanup docker tag mattermost-push-proxy:master-amd64 mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-amd64 # Push temp AMD64 image docker push mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-amd64 deploy-arm64: name: Deploy ARM64 runs-on: ubuntu-24.04-arm needs: [security-scan, build-arm64] if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]' permissions: contents: write id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push ARM64 image run: | # Build ARM64 image with temp tag for cleanup SHORT_SHA=${GITHUB_SHA:0:7} make build-image-arm64-with-tags # Retag with temp namespace for later cleanup docker tag mattermost-push-proxy:master-arm64 mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-arm64 # Push temp ARM64 image docker push mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-arm64 deploy: name: Deploy runs-on: ubuntu-latest needs: [deploy-amd64, deploy-arm64] if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]' permissions: contents: write id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create and push multi-arch manifest run: | # Create multi-platform manifest using commit SHA SHORT_SHA=${GITHUB_SHA:0:7} docker manifest create mattermost/mattermost-push-proxy:${SHORT_SHA} \ --amend mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-amd64 \ --amend mattermost/mattermost-push-proxy:temp-${GITHUB_SHA}-arm64 docker manifest push mattermost/mattermost-push-proxy:${SHORT_SHA} echo "✅ Clean unified multi-arch tag: mattermost/mattermost-push-proxy:${SHORT_SHA}" # Cleanup temp tags using Docker Hub API with org-level cleanup token echo "🗑️ Cleaning up temp tags from Docker Hub..." # Delete temp tags using Docker Hub API TEMP_AMD64_TAG="temp-${GITHUB_SHA}-amd64" TEMP_ARM64_TAG="temp-${GITHUB_SHA}-arm64" # Get Docker Hub API token using org-level cleanup token DOCKER_HUB_TOKEN=$(curl -s -X POST \ -H "Content-Type: application/json" \ -d '{"username": "matterbuild", "password": "${{ secrets.DOCKERHUB_CLEANUP_TOKEN }}"}' \ https://hub.docker.com/v2/users/login/ | jq -r .token) # Delete AMD64 temp tag curl -X DELETE \ -H "Authorization: JWT ${DOCKER_HUB_TOKEN}" \ "https://hub.docker.com/v2/repositories/mattermost/mattermost-push-proxy/tags/${TEMP_AMD64_TAG}/" \ && echo "✅ Deleted AMD64 temp tag" || echo "⚠️ AMD64 temp tag not found or already deleted" # Delete ARM64 temp tag curl -X DELETE \ -H "Authorization: JWT ${DOCKER_HUB_TOKEN}" \ "https://hub.docker.com/v2/repositories/mattermost/mattermost-push-proxy/tags/${TEMP_ARM64_TAG}/" \ && echo "✅ Deleted ARM64 temp tag" || echo "⚠️ ARM64 temp tag not found or already deleted" echo "✅ Temp tags cleaned up from Docker Hub" - name: Create release if: startsWith(github.ref, 'refs/tags/v') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: make github-release fips-deploy-amd64: name: FIPS Deploy AMD64 runs-on: ubuntu-latest needs: [fips-security-scan, fips-build-amd64] if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]' permissions: contents: write id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup Chainguard Identity uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0 with: identity: ${{ secrets.CHAINGUARD_IDENTITY }} - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push FIPS AMD64 image run: | # Build FIPS AMD64 image with temp tag for cleanup SHORT_SHA=${GITHUB_SHA:0:7} make build-image-fips-amd64-with-tags # Retag with temp namespace for later cleanup docker tag mattermost/mattermost-push-proxy-fips:master-amd64 mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-amd64 # Push temp FIPS AMD64 image docker push mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-amd64 fips-deploy-arm64: name: FIPS Deploy ARM64 runs-on: ubuntu-24.04-arm needs: [fips-security-scan, fips-build-arm64] if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]' permissions: contents: write id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Setup Chainguard Identity uses: chainguard-dev/setup-chainctl@c125f765e82b09a42af3185f3214465314d75c5d # v0.5.0 with: identity: ${{ secrets.CHAINGUARD_IDENTITY }} - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push FIPS ARM64 image run: | # Build FIPS ARM64 image with temp tag for cleanup SHORT_SHA=${GITHUB_SHA:0:7} make build-image-fips-arm64-with-tags # Retag with temp namespace for later cleanup docker tag mattermost/mattermost-push-proxy-fips:master-arm64 mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-arm64 # Push temp FIPS ARM64 image docker push mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-arm64 fips-deploy: name: FIPS Deploy runs-on: ubuntu-latest needs: [fips-deploy-amd64, fips-deploy-arm64] if: github.repository_owner == 'mattermost' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) && github.actor != 'dependabot[bot]' permissions: contents: write id-token: write steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: matterbuild password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create and push FIPS multi-arch manifest run: | # Create multi-platform manifest for FIPS using commit SHA SHORT_SHA=${GITHUB_SHA:0:7} docker manifest create mattermost/mattermost-push-proxy-fips:${SHORT_SHA} \ --amend mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-amd64 \ --amend mattermost/mattermost-push-proxy-fips:temp-${GITHUB_SHA}-arm64 docker manifest push mattermost/mattermost-push-proxy-fips:${SHORT_SHA} echo "✅ Clean unified FIPS multi-arch tag: mattermost/mattermost-push-proxy-fips:${SHORT_SHA}" # Cleanup temp FIPS tags using Docker Hub API echo "🗑️ Cleaning up temp FIPS tags from Docker Hub..." # Delete temp FIPS tags using Docker Hub API TEMP_AMD64_TAG="temp-${GITHUB_SHA}-amd64" TEMP_ARM64_TAG="temp-${GITHUB_SHA}-arm64" # Get Docker Hub API token using org-level cleanup token DOCKER_HUB_TOKEN=$(curl -s -X POST \ -H "Content-Type: application/json" \ -d '{"username": "matterbuild", "password": "${{ secrets.DOCKERHUB_CLEANUP_TOKEN }}"}' \ https://hub.docker.com/v2/users/login/ | jq -r .token) # Delete FIPS AMD64 temp tag curl -X DELETE \ -H "Authorization: JWT ${DOCKER_HUB_TOKEN}" \ "https://hub.docker.com/v2/repositories/mattermost/mattermost-push-proxy-fips/tags/${TEMP_AMD64_TAG}/" \ && echo "✅ Deleted FIPS AMD64 temp tag" || echo "⚠️ FIPS AMD64 temp tag not found or already deleted" # Delete FIPS ARM64 temp tag curl -X DELETE \ -H "Authorization: JWT ${DOCKER_HUB_TOKEN}" \ "https://hub.docker.com/v2/repositories/mattermost/mattermost-push-proxy-fips/tags/${TEMP_ARM64_TAG}/" \ && echo "✅ Deleted FIPS ARM64 temp tag" || echo "⚠️ FIPS ARM64 temp tag not found or already deleted" echo "✅ Temp FIPS tags cleaned up from Docker Hub" - name: Create FIPS release if: startsWith(github.ref, 'refs/tags/v') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: make github-release-fips - name: Cleanup run: make clean ================================================ FILE: .gitignore ================================================ logs .DS_Store dist # Build Targets .prebuild .prebuild-go .prebuild-jsx # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test .idea # Architecture specific extensions/prefixes [568vq].out _testmain.go *.exe *.test *.prof # Log files *mattermost.log # Vim temporary files *.swp # Default local file storage data/* api/data/* .agignore .ctags tags # Certificates *.pem *.cer *.p8 *.p12 *.key certs certs.bak *.env # Ignore binary /mattermost-push-proxy node_modules # Swagger output file index.html config/mattermost-push-proxy.json # Ignore config.json cmd/renew_apple_cert/convert_cert/config/** cmd/renew_apple_cert/convert_cert/config.json cmd/renew_apple_cert/create_csr/config/** cmd/renew_apple_cert/create_csr/config.json ================================================ FILE: .golangci.yml ================================================ # options for analysis running run: # timeout for analysis, e.g. 30s, 5m, default is 1m timeout: 5m linters-settings: gofmt: simplify: true govet: enable-all: true disable: - fieldalignment linters: disable-all: true enable: - gofmt - gosimple - govet - ineffassign - copyloopvar - staticcheck - unconvert - unused ================================================ FILE: CHANGELOG.md ================================================ # Mattermost Push Proxy Changelog ## 5.8.1 Release - Release Date: March 28, 2019 - Compatible with all versions of Mattermost server and mobile ### Compatibility - As of April 10, 2018, Google has deprecated the Google Cloud Messaging (GCM) service. The GCM server and client APIs are deprecated and will be removed as of April 11, 2019. You must [migrate GCM apps to Firebase Cloud Messaging (FCM)](https://developers.mattermost.com/contribute/mobile/push-notifications/migrate-gcm-fcm/), which inherits the reliable and scalable GCM infrastructure, plus many new features. ### Bug Fixes - FCM messages are sent as high priority to wake Android devices in doze mode ________________ ## 5.8 Release - Release Date: March 20, 2019 - Compatible with all versions of Mattermost server and mobile ### Combatibility - As of April 10, 2018, Google has deprecated the Google Cloud Messaging (GCM) service. The GCM server and client APIs are deprecated and will be removed as of April 11, 2019. You must [migrate GCM apps to Firebase Cloud Messaging (FCM)](https://developers.mattermost.com/contribute/mobile/push-notifications/migrate-gcm-fcm/), which inherits the reliable and scalable GCM infrastructure, plus many new features. ### Highlights - Adds support for Firebase Cloud Messaging ================================================ FILE: CONTRIBUTING.md ================================================ Please see: https://github.com/mattermost/mattermost-server/blob/master/CONTRIBUTING.md ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2015 Mattermost, Inc. All rights reserved. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # ==================================================================================== # Variables ## General Variables # Branch Variables PROTECTED_BRANCH := master CURRENT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) # Use repository name as application name APP_NAME := $(shell basename -s .git `git config --get remote.origin.url`) APP_COMMIT := $(shell git rev-parse HEAD) # Check if we are in protected branch, if yes use `protected_branch_name-sha` as app version. # Else check if we are in a release tag, if yes use the tag as app version, else use `dev-sha` as app version. APP_VERSION ?= $(shell if [ $(PROTECTED_BRANCH) = $(CURRENT_BRANCH) ]; then echo $(PROTECTED_BRANCH); else (git describe --abbrev=0 --exact-match --tags 2>/dev/null || echo dev-$(APP_COMMIT)) ; fi) APP_VERSION_NO_V := $(patsubst v%,%,$(APP_VERSION)) GIT_VERSION ?= $(shell git describe --tags --always --dirty) GIT_TREESTATE = clean DIFF = $(shell git diff --quiet >/dev/null 2>&1; if [ $$? -eq 1 ]; then echo "1"; fi) ifeq ($(DIFF), 1) GIT_TREESTATE = dirty endif GO_INSTALL = ./scripts/go_install.sh TOOLS_BIN_DIR := $(abspath bin) # Get current date and format like: 2022-04-27 11:32 BUILD_DATE := $(shell date +%Y-%m-%d\ %H:%M) # Get version information for plugins that depend on a semver version BUILD_HASH = $(shell git rev-parse --short HEAD) BUILD_TAG_LATEST = $(shell git describe --tags --match 'v*' --abbrev=0) BUILD_TAG_CURRENT = $(shell git tag --points-at HEAD) ## General Configuration Variables # We don't need make's built-in rules. MAKEFLAGS += --no-builtin-rules # Be pedantic about undefined variables. MAKEFLAGS += --warn-undefined-variables # Set help as default target .DEFAULT_GOAL := help # App Code location CONFIG_APP_CODE += ./ ## Docker Variables # Docker executable DOCKER := $(shell which docker) # Dockerfile's location DOCKER_FILE ?= ./docker/Dockerfile # Docker options to inherit for all docker run commands DOCKER_OPTS += --rm --platform "linux/amd64" # Registry to upload images DOCKER_REGISTRY ?= docker.io DOCKER_REGISTRY_REPO ?= mattermost/${APP_NAME}-daily # Registry credentials DOCKER_USER ?= user DOCKER_PASSWORD ?= password ## Latest Docker tags # if we are on a latest semver APP_VERSION tag, also push latest ifneq ($(shell echo $(APP_VERSION) | egrep '^v([0-9]+\.){0,2}(\*|[0-9]+)'),) ifeq ($(shell git tag -l --sort=v:refname | tail -n1),$(APP_VERSION)) LATEST_DOCKER_TAG = -t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:latest endif endif ## Docker Images DOCKER_IMAGE_GO ?= "golang:${GO_VERSION}" DOCKER_IMAGE_GOLINT ?= "golangci/golangci-lint:v1.64.4@sha256:e83b903d722c12402c9d88948a6cac42ea0e34bf336fc6a170ade9adeecb2d0e" DOCKER_IMAGE_DOCKERLINT ?= "hadolint/hadolint:v2.12.0" DOCKER_IMAGE_COSIGN ?= "bitnami/cosign:1.8.0@sha256:8c2c61c546258fffff18b47bb82a65af6142007306b737129a7bd5429d53629a" DOCKER_IMAGE_GH_CLI ?= "ghcr.io/supportpal/github-gh-cli:2.31.0@sha256:71371e36e62bd24ddd42d9e4c720a7e9954cb599475e24d1407af7190e2a5685" # To build FIPS-compliant push-proxy: make build-fips # Requires Docker to be installed and running FIPS_ENABLED ?= false BUILD_IMAGE_FIPS ?= cgr.dev/mattermost.com/go-msft-fips:1.24.6 BASE_IMAGE_FIPS ?= cgr.dev/mattermost.com/glibc-openssl-fips:15.1 ## Cosign Variables # The public key COSIGN_PUBLIC_KEY ?= akey # The private key COSIGN_KEY ?= akey # The passphrase used to decrypt the private key COSIGN_PASSWORD ?= password ## Go Variables # Go executable GO := $(shell which go) # Extract GO version from go.mod file GO_VERSION ?= $(shell grep -E '^go' go.mod | awk {'print $$2'}) # LDFLAGS GO_LDFLAGS += -X "github.com/mattermost/${APP_NAME}/internal/version.gitVersion=$(GIT_VERSION)" GO_LDFLAGS += -X "github.com/mattermost/${APP_NAME}/internal/version.buildHash=$(BUILD_HASH)" GO_LDFLAGS += -X "github.com/mattermost/${APP_NAME}/internal/version.buildTagLatest=$(BUILD_TAG_LATEST)" GO_LDFLAGS += -X "github.com/mattermost/${APP_NAME}/internal/version.buildTagCurrent=$(BUILD_TAG_CURRENT)" GO_LDFLAGS += -X "github.com/mattermost/${APP_NAME}/internal/version.gitTreeState=$(GIT_TREESTATE)" GO_LDFLAGS += -X "github.com/mattermost/${APP_NAME}/internal/version.buildDate=$(BUILD_DATE)" # Architectures to build for GO_BUILD_PLATFORMS ?= linux-amd64 linux-arm64 freebsd-amd64 GO_BUILD_PLATFORMS_ARTIFACTS = $(foreach cmd,$(addprefix go-build/,${APP_NAME}),$(addprefix $(cmd)-,$(GO_BUILD_PLATFORMS))) # Build options GO_BUILD_OPTS += -trimpath GO_TEST_OPTS += -v -timeout=180s # Temporary folder to output compiled binaries artifacts GO_OUT_BIN_DIR := ./dist ## Github Variables # A github access token that provides access to upload artifacts under releases GITHUB_TOKEN ?= a_token # Github organization GITHUB_ORG := mattermost # Most probably the name of the repo GITHUB_REPO := ${APP_NAME} ## FIPS Docker Variables APP_NAME_FIPS ?= mattermost/mattermost-push-proxy-fips APP_VERSION_FIPS ?= $(APP_VERSION)-fips APP_VERSION_NO_V_FIPS ?= $(APP_VERSION_NO_V)-fips ## Architecture Variables (default to ARM64 for local builds) TARGET_OS ?= linux TARGET_ARCH ?= arm64 OUTDATED_VER := master OUTDATED_BIN := go-mod-outdated OUTDATED_GEN := $(TOOLS_BIN_DIR)/$(OUTDATED_BIN) # ==================================================================================== # Colors BLUE := $(shell printf "\033[34m") YELLOW := $(shell printf "\033[33m") RED := $(shell printf "\033[31m") GREEN := $(shell printf "\033[32m") CYAN := $(shell printf "\033[36m") CNone := $(shell printf "\033[0m") # ==================================================================================== # Logger TIME_LONG = `date +%Y-%m-%d' '%H:%M:%S` TIME_SHORT = `date +%H:%M:%S` TIME = $(TIME_SHORT) INFO = echo ${TIME} ${BLUE}[ .. ]${CNone} WARN = echo ${TIME} ${YELLOW}[WARN]${CNone} ERR = echo ${TIME} ${RED}[FAIL]${CNone} OK = echo ${TIME} ${GREEN}[ OK ]${CNone} FAIL = (echo ${TIME} ${RED}[FAIL]${CNone} && false) # ==================================================================================== # Verbosity control hack VERBOSE ?= 0 AT_0 := @ AT_1 := AT = $(AT_$(VERBOSE)) # ==================================================================================== # Used for semver bumping CURRENT_VERSION := $(shell git describe --abbrev=0 --tags) VERSION_PARTS := $(subst ., ,$(subst v,,$(CURRENT_VERSION))) MAJOR := $(word 1,$(VERSION_PARTS)) MINOR := $(word 2,$(VERSION_PARTS)) PATCH := $(word 3,$(VERSION_PARTS)) # Check if current branch is protected define check_protected_branch @current_branch=$$(git rev-parse --abbrev-ref HEAD); \ if ! echo "$(PROTECTED_BRANCH)" | grep -wq "$$current_branch"; then \ echo "Error: Tagging is only allowed from $(PROTECTED_BRANCH) branch. You are on $$current_branch branch."; \ exit 1; \ fi endef # Check if there are pending pulls define check_pending_pulls @git fetch; \ current_branch=$$(git rev-parse --abbrev-ref HEAD); \ if [ "$$(git rev-parse HEAD)" != "$$(git rev-parse origin/$$current_branch)" ]; then \ echo "Error: Your branch is not up to date with upstream. Please pull the latest changes before performing a release"; \ exit 1; \ fi endef # ==================================================================================== # Targets help: ## to get help @echo "Usage:" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\ awk 'BEGIN {FS = ":.*?## "}; {printf "make ${CYAN}%-30s${CNone} %s\n", $$1, $$2}' .PHONY: build build: go-build-docker ## to build .PHONY: build-all build-all: build build-fips ## to build both normal and FIPS versions .PHONY: release release: build github-release ## to build and release artifacts .PHONY: release-fips release-fips: build-fips ## to build and release FIPS artifacts .PHONY: release-all release-all: build-all github-release-all ## to build and release both versions .PHONY: package package: go-build package-software ## to build, package .PHONY: package-fips package-fips: build-fips ## to build FIPS version .PHONY: package-all package-all: package package-fips ## to build, package both versions .PHONY: sign sign: docker-sign docker-verify ## to sign the artifact and perform verification .PHONY: lint lint: go-lint docker-lint ## to lint .PHONY: test test: go-test ## to test .PHONY: patch minor major patch: ## to bump patch version (semver) $(call check_protected_branch) $(call check_pending_pulls) @$(eval PATCH := $(shell echo $$(($(PATCH)+1)))) @$(INFO) Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH) git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)" git push origin v$(MAJOR).$(MINOR).$(PATCH) @$(OK) Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH) minor: ## to bump minor version (semver) $(call check_protected_branch) $(call check_pending_pulls) @$(eval MINOR := $(shell echo $$(($(MINOR)+1)))) @$(eval PATCH := 0) @$(INFO) Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH) git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)" git push origin v$(MAJOR).$(MINOR).$(PATCH) @$(OK) Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH) major: ## to bump major version (semver) $(call check_protected_branch) $(call check_pending_pulls) $(eval MAJOR := $(shell echo $$(($(MAJOR)+1)))) $(eval MINOR := 0) $(eval PATCH := 0) @$(INFO) Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH) git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)" git push origin v$(MAJOR).$(MINOR).$(PATCH) @$(OK) Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH) package-software: ## to package the binary @$(INFO) Packaging $(AT) for file in $(GO_OUT_BIN_DIR)/mattermost-push-proxy-*; do \ [[ "$$file" == *.tar.gz ]] && continue; \ target=$$(basename $$file); \ mkdir -p $(GO_OUT_BIN_DIR)/$${target}_temp/bin; \ cp -RL config $(GO_OUT_BIN_DIR)/$${target}_temp/config; \ echo $(APP_VERSION) > $(GO_OUT_BIN_DIR)/$${target}_temp/config/build.txt; \ cp LICENSE.txt NOTICE.txt README.md $(GO_OUT_BIN_DIR)/$${target}_temp; \ mkdir $(GO_OUT_BIN_DIR)/$${target}_temp/logs; \ mv $$file $(GO_OUT_BIN_DIR)/$${target}_temp/bin/mattermost-push-proxy; \ mv $(GO_OUT_BIN_DIR)/$${target}_temp $(GO_OUT_BIN_DIR)/$${target}; \ tar -czf $(GO_OUT_BIN_DIR)/$${target}.tar.gz -C $(GO_OUT_BIN_DIR) $${target}; \ rm -r $(GO_OUT_BIN_DIR)/$${target}; \ done @$(OK) Packaging .PHONY: package-software-fips package-software-fips: ## to package the FIPS binary @$(INFO) Packaging FIPS $(AT) for file in $(GO_OUT_BIN_DIR)/mattermost-push-proxy-*-fips; do \ [[ "$$file" == *.tar.gz ]] && continue; \ target=$$(basename $$file); \ mkdir -p $(GO_OUT_BIN_DIR)/$${target}_temp/bin; \ cp -RL config $(GO_OUT_BIN_DIR)/$${target}_temp/config; \ echo $(APP_VERSION)-fips > $(GO_OUT_BIN_DIR)/$${target}_temp/config/build.txt; \ cp LICENSE.txt NOTICE.txt README.md $(GO_OUT_BIN_DIR)/$${target}_temp; \ mkdir $(GO_OUT_BIN_DIR)/$${target}_temp/logs; \ mv $$file $(GO_OUT_BIN_DIR)/$${target}_temp/bin/mattermost-push-proxy; \ mv $(GO_OUT_BIN_DIR)/$${target}_temp $(GO_OUT_BIN_DIR)/$${target}; \ tar -czf $(GO_OUT_BIN_DIR)/$${target}.tar.gz -C $(GO_OUT_BIN_DIR) $${target}; \ rm -r $(GO_OUT_BIN_DIR)/$${target}; \ done @$(OK) Packaging FIPS .PHONY: docker-build docker-build: ## to build the docker image @$(INFO) Performing Docker build ${APP_NAME}:${APP_VERSION_NO_V} $(AT)$(DOCKER) buildx build \ --no-cache --pull --platform linux/amd64,linux/arm64 \ -f ${DOCKER_FILE} . \ -t ${APP_NAME}:${APP_VERSION_NO_V} || ${FAIL} @$(OK) Performing Docker build ${APP_NAME}:${APP_VERSION_NO_V} ## -------------------------------------- ## Regular Multi-Architecture Build Targets ## -------------------------------------- .PHONY: build-image-amd64-with-tags build-image-amd64-with-tags: go-build-amd64 package-software ## Build Docker image for AMD64 with tags @echo "Building mattermost-push-proxy Docker Image for AMD64" docker build --no-cache --pull \ --build-arg TARGETOS=linux \ --build-arg TARGETARCH=amd64 \ -f ${DOCKER_FILE} \ -t ${APP_NAME}:${APP_VERSION_NO_V}-amd64 \ -t ${APP_NAME}:${APP_VERSION_NO_V} \ . .PHONY: build-image-arm64-with-tags build-image-arm64-with-tags: go-build-arm64 package-software ## Build Docker image for ARM64 with tags @echo "Building mattermost-push-proxy Docker Image for ARM64" docker build --no-cache --pull \ --build-arg TARGETOS=linux \ --build-arg TARGETARCH=arm64 \ -f ${DOCKER_FILE} \ -t ${APP_NAME}:${APP_VERSION_NO_V}-arm64 \ -t ${APP_NAME}:${APP_VERSION_NO_V} \ . .PHONY: docker-build-parallel-with-tags docker-build-parallel-with-tags: ## Build Docker images for both architectures in parallel @echo "Building mattermost-push-proxy Docker Images for both platforms in parallel" $(MAKE) build-image-amd64-with-tags & $(MAKE) build-image-arm64-with-tags & wait @echo "Creating multi-platform manifests with clean tags" docker manifest create ${APP_NAME}:${APP_VERSION_NO_V} \ --amend ${APP_NAME}:${APP_VERSION_NO_V}-amd64 \ --amend ${APP_NAME}:${APP_VERSION_NO_V}-arm64 @echo "✅ Multi-platform manifests created" .PHONY: docker-push-with-tags docker-push-with-tags: ## Push Docker images with unified tags @echo "Pushing Docker images to registry" docker push ${APP_NAME}:${APP_VERSION_NO_V}-amd64 docker push ${APP_NAME}:${APP_VERSION_NO_V}-arm64 docker manifest push ${APP_NAME}:${APP_VERSION_NO_V} @echo "Cleaning up intermediate architecture-specific tags from registry" docker rmi ${APP_NAME}:${APP_VERSION_NO_V}-amd64 docker rmi ${APP_NAME}:${APP_VERSION_NO_V}-arm64 @echo "✅ Multi-platform images pushed with unified tags" @echo "✅ Intermediate architecture-specific tags cleaned up from registry" .PHONY: cleanup-tags cleanup-tags: ## Clean up intermediate architecture-specific tags from registry @echo "Cleaning up intermediate architecture-specific tags from registry" @echo "Removing AMD64 tag: ${APP_NAME}:${APP_VERSION_NO_V}-amd64" docker rmi ${APP_NAME}:${APP_VERSION_NO_V}-amd64 2>/dev/null || true @echo "Removing ARM64 tag: ${APP_NAME}:${APP_VERSION_NO_V}-arm64" docker rmi ${APP_NAME}:${APP_VERSION_NO_V}-arm64 2>/dev/null || true @echo "✅ Intermediate architecture-specific tags cleaned up from registry" .PHONY: build-image-fips build-image-fips: ## Build the FIPS docker image for mattermost-push-proxy @echo "Building mattermost-push-proxy FIPS Docker Image for $(TARGET_ARCH)" docker build --no-cache --pull \ --build-arg BUILD_IMAGE=$(BUILD_IMAGE_FIPS) \ --build-arg BASE_IMAGE=$(BASE_IMAGE_FIPS) \ --build-arg TARGETOS=$(TARGET_OS) \ --build-arg TARGETARCH=$(TARGET_ARCH) \ -f docker/Dockerfile.fips \ -t $(APP_NAME_FIPS):$(APP_VERSION_NO_V) . .PHONY: buildx-image-fips buildx-image-fips: ## Builds and pushes the FIPS docker image for mattermost-push-proxy @echo "Building mattermost-push-proxy FIPS Docker Image with buildx" docker buildx build --no-cache --pull \ --platform linux/amd64,linux/arm64 \ --build-arg BUILD_IMAGE=$(BUILD_IMAGE_FIPS) \ --build-arg BASE_IMAGE=$(BASE_IMAGE_FIPS) \ --build-arg TARGETOS=linux \ --build-arg TARGETARCH=amd64 \ -f docker/Dockerfile.fips \ -t $(APP_NAME_FIPS):$(APP_VERSION_NO_V) \ --push . ## -------------------------------------- ## FIPS Multi-Architecture Build Targets (Fast, Parallel) ## -------------------------------------- .PHONY: build-image-fips-amd64-with-tags build-image-fips-amd64-with-tags: ## Build FIPS Docker image for AMD64 with tags @echo "Building mattermost-push-proxy FIPS Docker Image for AMD64" docker build --no-cache --pull \ --build-arg BUILD_IMAGE=$(BUILD_IMAGE_FIPS) \ --build-arg BASE_IMAGE=$(BASE_IMAGE_FIPS) \ --build-arg TARGETOS=linux \ --build-arg TARGETARCH=amd64 \ -f docker/Dockerfile.fips \ -t $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64 \ -t $(APP_NAME_FIPS):$(APP_VERSION_NO_V) \ . .PHONY: build-image-fips-arm64-with-tags build-image-fips-arm64-with-tags: ## Build FIPS Docker image for ARM64 with tags @echo "Building mattermost-push-proxy FIPS Docker Image for ARM64" docker build --no-cache --pull \ --build-arg BUILD_IMAGE=$(BUILD_IMAGE_FIPS) \ --build-arg BASE_IMAGE=$(BASE_IMAGE_FIPS) \ --build-arg TARGETOS=linux \ --build-arg TARGETARCH=arm64 \ -f docker/Dockerfile.fips \ -t $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64 \ -t $(APP_NAME_FIPS):$(APP_VERSION_NO_V) \ . .PHONY: docker-build-fips-parallel-with-tags docker-build-fips-parallel-with-tags: ## Build FIPS Docker images for both architectures in parallel (FAST) @echo "Building mattermost-push-proxy FIPS Docker Images for both platforms in parallel" $(MAKE) build-image-fips-amd64-with-tags & $(MAKE) build-image-fips-arm64-with-tags & wait @echo "Creating multi-platform manifests with clean tags" docker manifest create $(APP_NAME_FIPS):$(APP_VERSION_NO_V) \ --amend $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64 \ --amend $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64 @echo "✅ Multi-platform manifests created" .PHONY: docker-push-fips-with-tags docker-push-fips-with-tags: ## Push FIPS Docker images with unified tags @echo "Pushing FIPS Docker images to registry" docker push $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64 docker push $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64 docker manifest push $(APP_NAME_FIPS):$(APP_VERSION_NO_V) @echo "Cleaning up intermediate architecture-specific tags from registry" docker rmi $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64 docker rmi $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64 @echo "✅ FIPS multi-platform images pushed with unified tags" @echo "✅ Intermediate architecture-specific tags cleaned up from registry" .PHONY: github-release-fips github-release-fips: ## Create GitHub release for FIPS version @echo "Creating GitHub release for FIPS version" gh release create v$(APP_VERSION_FIPS) \ --title "Release v$(APP_VERSION_FIPS) (FIPS)" \ --notes "FIPS-compliant release of mattermost-push-proxy" \ --target main @echo "✅ GitHub release created for FIPS version" .PHONY: cleanup-fips-tags cleanup-fips-tags: ## Clean up intermediate FIPS architecture-specific tags from registry @echo "Cleaning up intermediate FIPS architecture-specific tags from registry" @echo "Removing AMD64 tag: $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64" docker rmi $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-amd64 2>/dev/null || true @echo "Removing ARM64 tag: $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64" docker rmi $(APP_NAME_FIPS):$(APP_VERSION_NO_V)-arm64 2>/dev/null || true @echo "✅ Intermediate FIPS architecture-specific tags cleaned up from registry" .PHONY: docker-push docker-push: ## to push the docker image @$(INFO) Pushing to registry... $(AT)$(DOCKER) buildx build \ --no-cache --pull --platform linux/amd64,linux/arm64 \ -f ${DOCKER_FILE} . \ -t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V} $(LATEST_DOCKER_TAG) --push || ${FAIL} @$(OK) Pushing to registry $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V} .PHONY: docker-push-fips docker-push-fips: ## to push the FIPS docker image (builds for default TARGET_ARCH: $(TARGET_ARCH)) @$(INFO) Pushing FIPS to registry... $(AT)$(DOCKER) build \ --no-cache --pull \ --platform $(TARGET_OS)/$(TARGET_ARCH) \ -f ${DOCKER_FILE}.fips . \ -t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips || ${FAIL} $(AT)$(DOCKER) push $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips || ${FAIL} @$(OK) Pushing FIPS to registry $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips .PHONY: docker-push-fips-linux-amd64 docker-push-fips-linux-amd64: ## to push the FIPS docker image for Linux AMD64 @$(INFO) Pushing FIPS Linux AMD64 to registry... $(AT)$(DOCKER) build \ --no-cache --pull \ --platform linux/amd64 \ -f ${DOCKER_FILE}.fips . \ -t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-amd64 || ${FAIL} $(AT)$(DOCKER) push $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-amd64 || ${FAIL} @$(OK) Pushing FIPS Linux AMD64 to registry $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-amd64 .PHONY: docker-push-fips-linux-arm64 docker-push-fips-linux-arm64: ## to push the FIPS docker image for Linux ARM64 @$(INFO) Pushing FIPS Linux ARM64 to registry... $(AT)$(DOCKER) build \ --no-cache --pull \ --platform linux/arm64 \ -f ${DOCKER_FILE}.fips . \ -t $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-arm64 || ${FAIL} $(AT)$(DOCKER) push $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-arm64 || ${FAIL} @$(OK) Pushing FIPS Linux ARM64 to registry $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION_NO_V}-fips-linux-arm64 .PHONY: docker-push-fips-parallel docker-push-fips-parallel: ## to push FIPS docker images for both architectures in parallel @$(INFO) Pushing FIPS Docker images for both architectures in parallel $(AT)$(MAKE) docker-push-fips-linux-amd64 & $(AT)$(MAKE) docker-push-fips-linux-arm64 & wait @$(OK) FIPS Docker images pushed for both architectures .PHONY: docker-sign docker-sign: ## to sign the docker image @$(INFO) Signing the docker image... $(AT)echo "$${COSIGN_KEY}" > cosign.key && \ $(DOCKER) run ${DOCKER_OPTS} \ --entrypoint '/bin/sh' \ -v $(PWD):/app -w /app \ -e COSIGN_PASSWORD=${COSIGN_PASSWORD} \ -e HOME="/tmp" \ ${DOCKER_IMAGE_COSIGN} \ -c \ "echo Signing... && \ cosign login $(DOCKER_REGISTRY) -u ${DOCKER_USER} -p ${DOCKER_PASSWORD} && \ cosign sign --key cosign.key $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION}" || ${FAIL} # if we are on a latest semver APP_VERSION tag, also sign latest tag ifneq ($(shell echo $(APP_VERSION) | egrep '^v([0-9]+\.){0,2}(\*|[0-9]+)'),) ifeq ($(shell git tag -l --sort=v:refname | tail -n1),$(APP_VERSION)) $(DOCKER) run ${DOCKER_OPTS} \ --entrypoint '/bin/sh' \ -v $(PWD):/app -w /app \ -e COSIGN_PASSWORD=${COSIGN_PASSWORD} \ -e HOME="/tmp" \ ${DOCKER_IMAGE_COSIGN} \ -c \ "echo Signing... && \ cosign login $(DOCKER_REGISTRY) -u ${DOCKER_USER} -p ${DOCKER_PASSWORD} && \ cosign sign --key cosign.key $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:latest" || ${FAIL} endif endif $(AT)rm -f cosign.key || ${FAIL} @$(OK) Signing the docker image: $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION} .PHONY: docker-verify docker-verify: ## to verify the docker image @$(INFO) Verifying the published docker image... $(AT)echo "$${COSIGN_PUBLIC_KEY}" > cosign_public.key && \ $(DOCKER) run ${DOCKER_OPTS} \ --entrypoint '/bin/sh' \ -v $(PWD):/app -w /app \ ${DOCKER_IMAGE_COSIGN} \ -c \ "echo Verifying... && \ cosign verify --key cosign_public.key $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION}" || ${FAIL} # if we are on a latest semver APP_VERSION tag, also verify latest tag ifneq ($(shell echo $(APP_VERSION) | egrep '^v([0-9]+\.){0,2}(\*|[0-9]+)'),) ifeq ($(shell git tag -l --sort=v:refname | tail -n1),$(APP_VERSION)) $(DOCKER) run ${DOCKER_OPTS} \ --entrypoint '/bin/sh' \ -v $(PWD):/app -w /app \ ${DOCKER_IMAGE_COSIGN} \ -c \ "echo Verifying... && \ cosign verify --key cosign_public.key $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:latest" || ${FAIL} endif endif $(AT)rm -f cosign_public.key || ${FAIL} @$(OK) Verifying the published docker image: $(DOCKER_REGISTRY)/${DOCKER_REGISTRY_REPO}:${APP_VERSION} .PHONY: docker-sbom docker-sbom: ## to print a sbom report @$(INFO) Performing Docker sbom report... $(AT)$(DOCKER) sbom ${APP_NAME}:${APP_VERSION} || ${FAIL} @$(OK) Performing Docker sbom report .PHONY: docker-scan docker-scan: ## to print a vulnerability report @$(INFO) Performing Docker scan report... $(AT)$(DOCKER) scan ${APP_NAME}:${APP_VERSION} || ${FAIL} @$(OK) Performing Docker scan report .PHONY: docker-scout @$(INFO) Performing Docker scout report... $(AT)$(DOCKER) scout cves ${APP_NAME}:${APP_VERSION} || ${FAIL} @$(OK) Performing Docker scout report .PHONY: docker-lint docker-lint: ## to lint the Dockerfile @$(INFO) Dockerfile linting... $(AT)$(DOCKER) run -i ${DOCKER_OPTS} \ ${DOCKER_IMAGE_DOCKERLINT} \ < ${DOCKER_FILE} || ${FAIL} @$(OK) Dockerfile linting .PHONY: docker-login docker-login: ## to login to a container registry @$(INFO) Dockerd login to container registry ${DOCKER_REGISTRY}... $(AT) echo "${DOCKER_PASSWORD}" | $(DOCKER) login --password-stdin -u ${DOCKER_USER} $(DOCKER_REGISTRY) || ${FAIL} @$(OK) Dockerd login to container registry ${DOCKER_REGISTRY}... go-build: $(GO_BUILD_PLATFORMS_ARTIFACTS) ## to build binaries .PHONY: go-build-amd64 go-build-amd64: go-build/$(APP_NAME)-linux-amd64 ## Build AMD64 binary only .PHONY: go-build-arm64 go-build-arm64: go-build/$(APP_NAME)-linux-arm64 ## Build ARM64 binary only .PHONY: go-build go-build/%: @$(INFO) go build $*... $(AT)target="$*"; \ command="${APP_NAME}"; \ platform_ext="$${target#$$command-*}"; \ platform="$${platform_ext%.*}"; \ export GOOS="$${platform%%-*}"; \ export GOARCH="$${platform#*-}"; \ echo export GOOS=$${GOOS}; \ echo export GOARCH=$${GOARCH}; \ CGO_ENABLED=0 \ $(GO) build ${GO_BUILD_OPTS} \ -ldflags '${GO_LDFLAGS}' \ -o ${GO_OUT_BIN_DIR}/$* \ ${CONFIG_APP_CODE} || ${FAIL} @$(OK) go build $* .PHONY: go-build-docker go-build-docker: # to build binaries under a controlled docker dedicated go container using DOCKER_IMAGE_GO @$(INFO) go build docker $(AT)$(DOCKER) run \ -v $(PWD):/app -w /app \ $(DOCKER_IMAGE_GO) \ /bin/sh -c \ "cd /app && \ make go-build" || ${FAIL} @$(OK) go build docker ## -------------------------------------- ## FIPS Build Targets ## -------------------------------------- _build-fips-internal: ## Internal FIPS build target (used by Dockerfile.fips and build-fips) @echo "Building mattermost-push-proxy (FIPS)" @mkdir -p $(GO_OUT_BIN_DIR) GO111MODULE=on GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) CGO_ENABLED=1 go build -tags=fips,goexperiment.opensslcrypto -trimpath -o $(GO_OUT_BIN_DIR)/mattermost-push-proxy ./main.go .PHONY: build-fips build-fips: ## Build the mattermost-push-proxy with FIPS-compliant settings using containerized build @echo "Building mattermost-push-proxy (FIPS - $(TARGET_ARCH))" docker run --rm -v $(shell pwd):/app -w /app \ --entrypoint="" \ -e TARGET_OS=$(TARGET_OS) \ -e TARGET_ARCH=$(TARGET_ARCH) \ -e CGO_ENABLED=1 \ -e GOFIPS=1 \ -e GOEXPERIMENT=systemcrypto \ -e HOST_UID=$(shell id -u) \ -e HOST_GID=$(shell id -g) \ $(BUILD_IMAGE_FIPS) \ sh -c "cd /app && make _build-fips-internal TARGET_OS=\$$TARGET_OS TARGET_ARCH=\$$TARGET_ARCH && mv $(GO_OUT_BIN_DIR)/mattermost-push-proxy $(GO_OUT_BIN_DIR)/mattermost-push-proxy-fips-$(TARGET_ARCH) && chown \$$HOST_UID:\$$HOST_GID $(GO_OUT_BIN_DIR)/mattermost-push-proxy-fips-$(TARGET_ARCH)" .PHONY: build-fips-amd64 build-fips-amd64: ## Build the mattermost-push-proxy with FIPS-compliant settings for AMD64 $(MAKE) build-fips TARGET_ARCH=amd64 .PHONY: build-fips-arm64 build-fips-arm64: ## Build the mattermost-push-proxy with FIPS-compliant settings for ARM64 $(MAKE) build-fips TARGET_ARCH=arm64 .PHONY: build-fips-all build-fips-all: build-fips-amd64 build-fips-arm64 ## Build FIPS binaries for both architectures .PHONY: go-run go-run: ## to run locally for development @$(INFO) running locally... $(AT)$(GO) run ${GO_BUILD_OPTS} ${CONFIG_APP_CODE} || ${FAIL} @$(OK) running locally .PHONY: go-test go-test: ## to run tests @$(INFO) testing... $(AT)$(DOCKER) run ${DOCKER_OPTS} \ -v $(PWD):/app -w /app \ $(DOCKER_IMAGE_GO) \ /bin/sh -c \ "cd /app && \ go test ${GO_TEST_OPTS} ./... " || ${FAIL} @$(OK) testing .PHONY: go-mod-check go-mod-check: ## to check go mod files consistency @$(INFO) Checking go mod files consistency... $(AT)$(GO) mod tidy $(AT)git --no-pager diff --exit-code go.mod go.sum || \ (${WARN} Please run "go mod tidy" and commit the changes in go.mod and go.sum. && ${FAIL} ; exit 128 ) @$(OK) Checking go mod files consistency .PHONY: go-lint go-lint: ## to lint go code @$(INFO) App linting... $(AT)$(DOCKER) run ${DOCKER_OPTS} \ -v $(PWD):/app -w /app \ ${DOCKER_IMAGE_GOLINT} \ golangci-lint run ./... || ${FAIL} @$(OK) App linting .PHONY: go-doc go-doc: ## to generate documentation @$(INFO) Generating Documentation... $(AT)$(GO) run ./scripts/env_config.go ./docs/env_config.md || ${FAIL} @$(OK) Generating Documentation .PHONY: check-modules check-modules: $(OUTDATED_GEN) ## Check outdated modules @echo Checking outdated modules $(GO) list -mod=mod -u -m -json all | $(OUTDATED_GEN) -update -direct .PHONY: update-modules update-modules: ## Update all modules to latest versions @echo Updating modules $(GO) get -u ./... $(GO) mod tidy .PHONY: scan scan: ## Scan Docker image for vulnerabilities using Docker Scout @echo Running Docker Scout vulnerability scan @if ! docker images -q ${APP_NAME}:${APP_VERSION_NO_V} | grep -q .; then \ echo "❌ Image ${APP_NAME}:${APP_VERSION_NO_V} not found locally. Please build it first with:"; \ echo " make build-image-amd64-with-tags (or build-image-arm64-with-tags)"; \ exit 1; \ fi docker scout cves ${APP_NAME}:${APP_VERSION_NO_V} .PHONY: scan-fips scan-fips: ## Scan FIPS Docker image for vulnerabilities using Docker Scout @echo Running Docker Scout vulnerability scan for FIPS image @if ! docker images -q $(APP_NAME_FIPS):$(APP_VERSION_NO_V) | grep -q .; then \ echo "❌ Image $(APP_NAME_FIPS):$(APP_VERSION_NO_V) not found locally. Please build it first with:"; \ echo " make build-image-fips-amd64-with-tags (or build-image-fips-arm64-with-tags)"; \ exit 1; \ fi docker scout cves $(APP_NAME_FIPS):$(APP_VERSION_NO_V) .PHONY: grype grype: ## Scan Docker image for vulnerabilities using Grype @echo Running Grype vulnerability scan @if ! docker images -q ${APP_NAME}:${APP_VERSION_NO_V} | grep -q .; then \ echo "❌ Image ${APP_NAME}:${APP_VERSION_NO_V} not found locally. Please build it first with:"; \ echo " make build-image-amd64-with-tags (or build-image-arm64-with-tags)"; \ exit 1; \ fi grype docker:${APP_NAME}:${APP_VERSION_NO_V} -o table --only-fixed .PHONY: grype-fips grype-fips: ## Scan FIPS Docker image for vulnerabilities using Grype @echo Running Grype vulnerability scan for FIPS image @if ! docker images -q $(APP_NAME_FIPS):$(APP_VERSION_NO_V) | grep -q .; then \ echo "❌ Image $(APP_NAME_FIPS):$(APP_VERSION_NO_V) not found locally. Please build it first with:"; \ echo " make build-image-fips-amd64-with-tags (or build-image-fips-arm64-with-tags)"; \ exit 1; \ fi grype docker:$(APP_NAME_FIPS):$(APP_VERSION_NO_V) -o table --only-fixed .PHONY: security-all security-all: ## Run all vulnerability scans (Docker Scout and Grype) for both regular and FIPS images @echo "🔍 Running comprehensive security scans for all images..." @echo "" @echo "==========================================" @echo "📊 Docker Scout - Regular Image" @echo "==========================================" $(MAKE) scan @echo "" @echo "==========================================" @echo "📊 Docker Scout - FIPS Image" @echo "==========================================" $(MAKE) scan-fips @echo "" @echo "==========================================" @echo "🛡️ Grype - Regular Image" @echo "==========================================" $(MAKE) grype @echo "" @echo "==========================================" @echo "🛡️ Grype - FIPS Image" @echo "==========================================" $(MAKE) grype-fips @echo "" @echo "✅ All security scans completed!" .PHONY: security-build-and-scan security-build-and-scan: ## Build images and run comprehensive security scans @echo "🚀 Building images and running comprehensive security scans..." @echo "" @echo "==========================================" @echo "🔨 Building Regular ARM64 Image" @echo "==========================================" $(MAKE) build-image-arm64-with-tags @echo "" @echo "==========================================" @echo "🔨 Building FIPS ARM64 Image" @echo "==========================================" $(MAKE) build-image-fips-arm64-with-tags @echo "" @echo "🔍 Starting security scans..." $(MAKE) security-all .PHONY: github-release github-release: ## to publish a release and relevant artifacts to GitHub @$(INFO) Generating github-release http://github.com/$(GITHUB_ORG)/$(GITHUB_REPO)/releases/tag/$(APP_VERSION) ... ifeq ($(shell echo $(APP_VERSION) | egrep '^v([0-9]+\.){0,2}(\*|[0-9]+)'),) $(error "We only support releases from semver tags") else $(AT)$(DOCKER) run \ -v $(PWD):/app -w /app \ -e GITHUB_TOKEN=${GITHUB_TOKEN} \ $(DOCKER_IMAGE_GH_CLI) \ /bin/sh -c \ "git config --global --add safe.directory /app && cd /app && \ gh release create $(APP_VERSION) --generate-notes $(GO_OUT_BIN_DIR)/*" || ${FAIL} endif @$(OK) Generating github-release http://github.com/$(GITHUB_ORG)/$(GITHUB_REPO)/releases/tag/$(APP_VERSION) ... .PHONY: github-release-all github-release-all: ## to publish both normal and FIPS releases to GitHub @$(INFO) Generating both releases... $(AT)$(MAKE) github-release $(AT)$(MAKE) github-release-fips @$(OK) Generating both releases .PHONY: clean clean: ## to clean-up @$(INFO) cleaning /${GO_OUT_BIN_DIR} folder... $(AT)rm -rf ${GO_OUT_BIN_DIR} || ${FAIL} @$(OK) cleaning /${GO_OUT_BIN_DIR} folder ## -------------------------------------- ## Tooling Binaries ## -------------------------------------- $(OUTDATED_GEN): ## Build go-mod-outdated. GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) github.com/psampaz/go-mod-outdated $(OUTDATED_BIN) $(OUTDATED_VER) ================================================ FILE: NOTICE.txt ================================================ Mattermost Platform © 2015 Mattermost, Inc. All Rights Reserved. See LICENSE.txt for license information. NOTICES: -------- ================================================ FILE: README.md ================================================ # Mattermost Push Proxy ![CircleCI branch](https://img.shields.io/circleci/project/github/mattermost/mattermost-push-proxy/master.svg) See https://developers.mattermost.com/contribute/mobile/push-notifications/service/ # How to Release To trigger a release of Mattermost Push-Proxy, follow these steps: 1. **For Patch Release:** Run the following command: ``` make patch ``` This will release a patch change. 2. **For Minor Release:** Run the following command: ``` make minor ``` This will release a minor change. 3. **For Major Release:** Run the following command: ``` make major ``` This will release a major change. ================================================ FILE: bin/.gitignore ================================================ # Ignore all files in this directory * # Except this one !.gitignore ================================================ FILE: build/setup.txt ================================================ #!/bin/bash # copy following lines to /etc/init/matter-push-proxy.conf # start on runlevel [2345] # stop on runlevel [016] # respawn # chdir /home/ubuntu/matter-push-proxy # setuid ubuntu # console output # exec bin/push-proxy | logger sudo stop matter-push-proxy rm -f matter-push-proxy.tar.gz rm -rf ~/matter-push-proxy wget https://github.com/mattermost/push-proxy/releases/download/v0.1.0/matter-push-proxy.tar.gz mkdir -p ~/matter-push-proxy tar -C ~/ -xzf matter-push-proxy.tar.gz cp config-push-proxy.json ~/matter-push-proxy/config/config-push-proxy.json sudo start matter-push-proxy sleep 10 curl localhost:8066 ================================================ FILE: config/mattermost-push-proxy.sample.json ================================================ { "ListenAddress":":8066", "ThrottlePerSec":300, "ThrottleMemoryStoreSize":50000, "ThrottleVaryByHeader":"X-Forwarded-For", "EnableMetrics": false, "SendTimeoutSec": 30, "RetryTimeoutSec": 8, "ApplePushSettings":[ { "Type":"apple", "ApplePushUseDevelopment":false, "ApplePushCertPrivate":"", "ApplePushCertPassword":"", "ApplePushTopic":"com.mattermost.Mattermost", "AppleAuthKeyFile": "", "AppleAuthKeyID": "", "AppleTeamID": "" }, { "Type":"apple_rn", "ApplePushUseDevelopment":false, "ApplePushCertPrivate":"", "ApplePushCertPassword":"", "ApplePushTopic":"com.mattermost.react.native", "AppleAuthKeyFile": "", "AppleAuthKeyID": "", "AppleTeamID": "" } ], "AndroidPushSettings": [ { "Type":"android", "ServiceFileLocation":"" }, { "Type":"android_rn", "ServiceFileLocation":"" } ], "EnableConsoleLog": true, "EnableFileLog": false, "LogFileLocation": "", "LogFormat": "plain" } ================================================ FILE: docker/Dockerfile ================================================ FROM --platform=${TARGETPLATFORM} ubuntu:noble-20250529 AS builder ARG TARGETARCH # Setting bash as our shell, and enabling pipefail option SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Copying tarball WORKDIR /mattermost-push-proxy COPY dist/mattermost-push-proxy-linux-${TARGETARCH}.tar.gz /mattermost-push-proxy-linux.tar.gz RUN tar -xf /mattermost-push-proxy-linux.tar.gz --strip-components=1 -C /mattermost-push-proxy FROM --platform=${TARGETPLATFORM} ubuntu:noble-20250529 # Install needed packages and indirect dependencies # hadolint ignore=DL3008 RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ ca-certificates \ libffi-dev \ netcat-openbsd \ tzdata \ && rm -rf /var/lib/apt/lists/* # Coyping needed files from previous stage COPY --from=builder /mattermost-push-proxy/ /mattermost-push-proxy/ COPY docker/entrypoint /usr/local/bin/ RUN chown -R nobody:nogroup /mattermost-push-proxy USER nobody WORKDIR /mattermost-push-proxy ENV PUSH_PROXY=/mattermost-push-proxy/bin/mattermost-push-proxy EXPOSE 8066 VOLUME ["/mattermost-push-proxy/config", "/mattermost-push-proxy/certs"] ENTRYPOINT ["/usr/local/bin/entrypoint"] ================================================ FILE: docker/Dockerfile.fips ================================================ # Build the mattermost-push-proxy (FIPS version) ARG BUILD_IMAGE=cgr.dev/mattermost.com/go-msft-fips:1.24.6 ARG BASE_IMAGE=cgr.dev/mattermost.com/glibc-openssl-fips:15.2 FROM ${BUILD_IMAGE} AS builder ARG TARGETARCH ARG TARGETOS WORKDIR /workspace COPY . . # Copy config file to workspace for later use COPY config/mattermost-push-proxy.sample.json /workspace/mattermost-push-proxy.json # Build with FIPS-compliant settings ENV CGO_ENABLED=1 ENV GOFIPS=1 ENV GOEXPERIMENT=systemcrypto RUN make _build-fips-internal TARGET_OS=$TARGETOS TARGET_ARCH=$TARGETARCH FROM ${BASE_IMAGE} LABEL name="Mattermost Push Proxy (FIPS)" \ maintainer="sre@mattermost.com" \ vendor="Mattermost" \ distribution-scope="public" \ url="https://mattermost.dev" \ io.k8s.description="Mattermost Push Proxy service for FIPS-compliant environments" \ io.k8s.display-name="Mattermost Push Proxy (FIPS)" \ io.openshift.tags="mattermost,push-proxy,fips" \ summary="FIPS-compliant Mattermost Push Proxy service" \ description="Mattermost Push Proxy service for FIPS-compliant environments. This is a FIPS-compliant version." \ com.mattermost.fips="true" \ com.mattermost.fips.version="140-2" WORKDIR /mattermost-push-proxy # Copy files - directories will be created automatically COPY --from=builder /workspace/dist/mattermost-push-proxy /mattermost-push-proxy/bin/mattermost-push-proxy COPY --from=builder /workspace/mattermost-push-proxy.json /mattermost-push-proxy/config/mattermost-push-proxy.json USER 65532 EXPOSE 8066 VOLUME ["/mattermost-push-proxy/config", "/mattermost-push-proxy/certs"] # Use the binary from bin directory ENTRYPOINT ["/mattermost-push-proxy/bin/mattermost-push-proxy"] ================================================ FILE: docker/entrypoint ================================================ #!/bin/sh -e exec ${PUSH_PROXY} $@ ================================================ FILE: go.mod ================================================ module github.com/mattermost/mattermost-push-proxy go 1.24.6 require ( firebase.google.com/go/v4 v4.18.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/kyokomi/emoji v2.2.4+incompatible github.com/mattermost/mattermost/server/public v0.1.16 github.com/prometheus/client_golang v1.23.0 github.com/prometheus/common v0.65.0 github.com/sideshow/apns2 v0.25.0 github.com/stretchr/testify v1.10.0 golang.org/x/net v0.43.0 golang.org/x/oauth2 v0.30.0 google.golang.org/api v0.248.0 gopkg.in/throttled/throttled.v1 v1.0.0 ) require ( cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.121.6 // indirect cloud.google.com/go/auth v0.16.5 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.8.0 // indirect cloud.google.com/go/firestore v1.18.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/storage v1.56.1 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/PuerkitoBio/boom v0.0.0-20140219125548-fecdef1c97ca // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/garyburd/redigo v1.6.4 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/mattermost/logr/v2 v2.0.22 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/rakyll/pb v0.0.0-20160123035540-8d46b8b097ef // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/genproto v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/grpc v1.75.0 // indirect google.golang.org/protobuf v1.36.7 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.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.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/storage v1.56.1 h1:n6gy+yLnHn0hTwBFzNn8zJ1kqWfR91wzdM8hjRF4wP0= cloud.google.com/go/storage v1.56.1/go.mod h1:C9xuCZgFl3buo2HZU/1FncgvvOgTAs/rnh4gF4lMg0s= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw= firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/PuerkitoBio/boom v0.0.0-20140219125548-fecdef1c97ca h1:jv7AlMqwTYg92zzES80+2pXD0bPY5kGT3AhFKdXLLdI= github.com/PuerkitoBio/boom v0.0.0-20140219125548-fecdef1c97ca/go.mod h1:BUNf81ELJpN4dRN2LS5r1fkTSLkczJubIXjJv04ib70= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20201120081800-1786d5ef83d4/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 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/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 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/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg= github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 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 v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 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/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4= github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r/lP4= github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s= github.com/mattermost/mattermost/server/public v0.1.16 h1:CtTzNnuFCNndUaNfQse5NnvzfBZt+8P+wOU7OMn07zg= github.com/mattermost/mattermost/server/public v0.1.16/go.mod h1:hvxMXqfao9JDHM3auk8MLl7DD6jAuG0q27Kf9y6z1r0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 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.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rakyll/pb v0.0.0-20160123035540-8d46b8b097ef h1:EzrUZp6kh2we8o30Qhx5SJxg7jaiVwlq3WaecTc4BOo= github.com/rakyll/pb v0.0.0-20160123035540-8d46b8b097ef/go.mod h1:6Wyuoi/kwXz73J3mwjVSCvIr9QprJ7Q9GrogPi1QZ+8= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/sideshow/apns2 v0.25.0 h1:XOzanncO9MQxkb03T/2uU2KcdVjYiIf0TMLzec0FTW4= github.com/sideshow/apns2 v0.25.0/go.mod h1:7Fceu+sL0XscxrfLSkAoH6UtvKefq3Kq1n4W3ayQZqE= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME= github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20170512130425-ab89591268e0/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/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-20190313220215-9f648a60d977/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 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-20190227155943-e225da77a7e6/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y= google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20250818200422-3122310a409c h1:ZERoum3uuqL0PRSc6SXielu26FN96T4BUGaaW0oL+c8= google.golang.org/genproto v0.0.0-20250818200422-3122310a409c/go.mod h1:Q8kep885BJnK3Jt6QZXIFeLHSzoAQtlI1CCloQigiyU= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 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/throttled/throttled.v1 v1.0.0 h1:HW4VuZPcA2x88dJSf3T7GLTOwYCrdQcYDEC65ZEX2mQ= gopkg.in/throttled/throttled.v1 v1.0.0/go.mod h1:UIVpydfpoUKqbHErIHUoEnuOj9KVPAmS88/iAIqBScE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= ================================================ FILE: internal/version/version.go ================================================ package version import ( "fmt" "runtime" "strings" "text/tabwriter" ) // Base version information. // // This is the fallback data used when version information from git is not // provided via go ldflags (e.g. via Makefile). var ( // Output of "git describe". The prerequisite is that the branch should be // tagged using the correct versioning strategy. gitVersion string = "devel" // short SHA1 from git, output of $(git rev-parse --short HEAD) buildHash = "unknown" // the most recent v* tag in the current branch (or its ancestors) buildTagLatest = "unknown" // the current commit's v* tag buildTagCurrent = "unknown" // State of git tree, either "clean" or "dirty" gitTreeState = "unknown" // Build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') buildDate = "unknown" ) func GetVersion() error { v := VersionInfo() res := v.String() fmt.Println(res) return nil } type Info struct { GitVersion string `json:"-"` BuildHash string `json:"hash"` BuildVersion string `json:"version"` GitTreeState string `json:"-"` BuildDate string `json:"-"` GoVersion string `json:"-"` Compiler string `json:"-"` Platform string `json:"-"` } func VersionInfo() Info { // These variables typically come from -ldflags settings and in // their absence fallback to the global defaults set above. // Create the semver version based on the state of the current commit or its branch. // Use the first version we find. var version string tags := strings.Fields(buildTagCurrent) for _, t := range tags { if strings.HasPrefix(t, "v") { version = t break } } if version == "" { version = buildTagLatest + "+" + buildHash } version = strings.TrimPrefix(version, "v") return Info{ GitVersion: gitVersion, BuildHash: buildHash, BuildVersion: version, GitTreeState: gitTreeState, BuildDate: buildDate, GoVersion: runtime.Version(), Compiler: runtime.Compiler, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } } // String returns the string representation of the version info func (i *Info) String() string { b := strings.Builder{} w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0) fmt.Fprintf(w, "GitVersion:\t%s\n", i.GitVersion) fmt.Fprintf(w, "BuildHash:\t%s\n", i.BuildHash) fmt.Fprintf(w, "BuildVersion:\t%s\n", i.BuildVersion) fmt.Fprintf(w, "GitTreeState:\t%s\n", i.GitTreeState) fmt.Fprintf(w, "BuildDate:\t%s\n", i.BuildDate) fmt.Fprintf(w, "GoVersion:\t%s\n", i.GoVersion) fmt.Fprintf(w, "Compiler:\t%s\n", i.Compiler) fmt.Fprintf(w, "Platform:\t%s\n", i.Platform) w.Flush() // #nosec return b.String() } ================================================ FILE: main.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package main import ( "flag" "log" "os" "os/signal" "syscall" "github.com/mattermost/mattermost-push-proxy/internal/version" "github.com/mattermost/mattermost-push-proxy/server" ) var ( flagConfigFile string falgVersion bool ) func main() { flag.StringVar(&flagConfigFile, "config", "mattermost-push-proxy.json", "") flag.BoolVar(&falgVersion, "version", false, "") flag.Parse() if falgVersion { if err := version.GetVersion(); err != nil { log.Fatal(err) } os.Exit(0) } fileName := server.FindConfigFile(flagConfigFile) cfg, err := server.LoadConfig(fileName) if err != nil { // We just do a hard exit, because the app won't be able to start without a config. log.Fatal(err) } logger, err := server.NewLogger(cfg) defer func() { if logger != nil { _ = logger.Shutdown() } }() if err != nil { log.Fatal(err) } logger.Info("Loading " + fileName) srv := server.New(cfg, logger) srv.Start() // wait for kill signal before attempting to gracefully shutdown // the running service stopChan := make(chan os.Signal, 1) signal.Notify(stopChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) <-stopChan srv.Stop() } ================================================ FILE: package.json ================================================ { "name": "mattermost-push-proxy", "version": "1.0.0", "description": "Repository for mattermost push proxy", "main": "", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "validate": "swagger-cli validate ./swagger/spec.yaml", "build": "redoc-cli bundle -t ./swagger/template.hbs ./swagger/spec.yaml -o index.html --options.suppressWarnings", "serve": "redoc-cli serve -t ./swagger/template.hbs ./swagger/spec.yaml -o index.html --options.suppressWarnings --ssr --watch" }, "repository": { "type": "git", "url": "git+https://github.com/mattermost/mattermost-push-proxy.git" }, "author": "", "license": "ISC", "bugs": { "url": "https://github.com/mattermost/mattermost-push-proxy/issues" }, "homepage": "https://github.com/mattermost/mattermost-push-proxy#readme", "dependencies": { "redoc-cli": "^0.9.9", "swagger-cli": "^4.0.4" } } ================================================ FILE: scripts/go_install.sh ================================================ #!/usr/bin/env bash # Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. # See LICENSE.txt for license information. set -o errexit set -o nounset set -o pipefail if [ -z "${1}" ]; then echo "must provide module as first parameter" exit 1 fi if [ -z "${2}" ]; then echo "must provide binary name as second parameter" exit 1 fi if [ -z "${3}" ]; then echo "must provide version as third parameter" exit 1 fi if [ -z "${GOBIN}" ]; then echo "GOBIN is not set. Must set GOBIN to install the bin in a specified directory." exit 1 fi tmp_dir=$(mktemp -d -t goinstall_XXXXXXXXXX) function clean { rm -rf "${tmp_dir}" } trap clean EXIT rm "${GOBIN}/${2}"* || true cd "${tmp_dir}" # create a new module in the tmp directory go mod init fake/mod # install the golang module specified as the first argument go install -tags tools "${1}@${3}" mv "${GOBIN}/${2}" "${GOBIN}/${2}-${3}" ln -sf "${GOBIN}/${2}-${3}" "${GOBIN}/${2}" ================================================ FILE: server/android_notification_server.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "context" "encoding/json" "errors" "fmt" "os" "reflect" "strconv" "time" firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/messaging" "github.com/kyokomi/emoji" "golang.org/x/oauth2/google" "google.golang.org/api/option" "github.com/mattermost/mattermost/server/public/shared/mlog" ) const ( apnsAuthError = "APNS_AUTH_ERROR" internalError = "INTERNAL" thirdPartyAuthError = "THIRD_PARTY_AUTH_ERROR" invalidArgument = "INVALID_ARGUMENT" quotaExceeded = "QUOTA_EXCEEDED" unregistered = "UNREGISTERED" unavailable = "UNAVAILABLE" tokenSourceError = "TOKEN_SOURCE_ERROR" ) const ( scope = "https://www.googleapis.com/auth/firebase.messaging" ) type AndroidNotificationServer struct { metrics *metrics logger *mlog.Logger AndroidPushSettings AndroidPushSettings client *messaging.Client sendTimeout time.Duration retryTimeout time.Duration } // serviceAccount contains a subset of the fields in service-account.json. // It is mainly used to extract the projectID and client email for authentication. type serviceAccount struct { Type string `json:"type"` ProjectID string `json:"project_id"` ClientEmail string `json:"client_email"` ClientID string `json:"client_id"` AuthURI string `json:"auth_uri"` TokenURI string `json:"token_uri"` } func NewAndroidNotificationServer(settings AndroidPushSettings, logger *mlog.Logger, metrics *metrics, sendTimeoutSecs int, retryTimeoutSecs int) *AndroidNotificationServer { return &AndroidNotificationServer{ AndroidPushSettings: settings, metrics: metrics, logger: logger, sendTimeout: time.Duration(sendTimeoutSecs) * time.Second, retryTimeout: time.Duration(retryTimeoutSecs) * time.Second, } } func (me *AndroidNotificationServer) Initialize() error { me.logger.Info("Initializing Android notification server", mlog.String("type", me.AndroidPushSettings.Type)) if me.AndroidPushSettings.AndroidAPIKey != "" { me.logger.Info("AndroidPushSettings.AndroidAPIKey is no longer used. Please remove this config value.") } if me.AndroidPushSettings.ServiceFileLocation == "" { return errors.New("Android push notifications not configured. Missing ServiceFileLocation.") } jsonKey, err := os.ReadFile(me.AndroidPushSettings.ServiceFileLocation) if err != nil { return fmt.Errorf("error reading service file: %v", err) } cfg, err := google.JWTConfigFromJSON(jsonKey, scope) if err != nil { return fmt.Errorf("error getting JWT config: %v", err) } var serviceAcc serviceAccount err = json.Unmarshal(jsonKey, &serviceAcc) if err != nil { return fmt.Errorf("error parsing service account JSON: %v", err) } opt := option.WithTokenSource(cfg.TokenSource(context.Background())) conf := &firebase.Config{ ProjectID: serviceAcc.ProjectID, ServiceAccountID: serviceAcc.ClientEmail, } app, err := firebase.NewApp(context.Background(), conf, opt) if err != nil { return fmt.Errorf("error initializing app: %v", err) } client, err := app.Messaging(context.Background()) if err != nil { return fmt.Errorf("error initializing client: %v", err) } me.client = client return nil } func (me *AndroidNotificationServer) SendNotification(msg *PushNotification) PushResponse { pushType := msg.Type data := map[string]string{ "ack_id": msg.AckID, "type": pushType, "sub_type": msg.SubType, "version": msg.Version, "channel_id": msg.ChannelID, "is_crt_enabled": strconv.FormatBool(msg.IsCRTEnabled), "server_id": msg.ServerID, "category": msg.Category, } if msg.Badge != -1 { data["badge"] = strconv.Itoa(msg.Badge) } if msg.RootID != "" { data["root_id"] = msg.RootID } if msg.Signature == "" { data["signature"] = "NO_SIGNATURE" } else { data["signature"] = msg.Signature } if msg.IsIDLoaded { data["post_id"] = msg.PostID data["message"] = msg.Message data["id_loaded"] = "true" data["sender_id"] = msg.SenderID data["sender_name"] = "Someone" data["team_id"] = msg.TeamID } else if pushType == PushTypeMessage || pushType == PushTypeSession { data["team_id"] = msg.TeamID data["sender_id"] = msg.SenderID data["sender_name"] = msg.SenderName data["message"] = emoji.Sprint(msg.Message) data["channel_name"] = msg.ChannelName data["post_id"] = msg.PostID data["override_username"] = msg.OverrideUsername data["override_icon_url"] = msg.OverrideIconURL data["from_webhook"] = msg.FromWebhook } if me.metrics != nil { me.metrics.incrementNotificationTotal(PushNotifyAndroid, pushType) } fcmMsg := &messaging.Message{ Token: msg.DeviceID, Data: data, Android: &messaging.AndroidConfig{ Priority: "high", }, } me.logger.Info( "Sending android push notification", mlog.String("device", me.AndroidPushSettings.Type), mlog.String("type", msg.Type), mlog.String("ack_id", msg.AckID), ) err := me.SendNotificationWithRetry(fcmMsg) if err != nil { errorCode, hasStatusCode := getErrorCode(err) if !hasStatusCode { errorCode = "NONE" } me.logger.Error( "Failed to send FCM push", mlog.String("sid", msg.ServerID), mlog.String("did", msg.DeviceID), mlog.Err(err), mlog.String("type", me.AndroidPushSettings.Type), mlog.String("errorCode", errorCode), ) if messaging.IsUnregistered(err) || messaging.IsSenderIDMismatch(err) { me.logger.Info("Android response failure sending remove code", mlog.String("type", me.AndroidPushSettings.Type)) if me.metrics != nil { me.metrics.incrementRemoval(PushNotifyAndroid, pushType, unregistered) } return NewRemovePushResponse() } var reason string switch { case messaging.IsInternal(err): reason = internalError case messaging.IsInvalidArgument(err): reason = invalidArgument case messaging.IsQuotaExceeded(err): reason = quotaExceeded case messaging.IsThirdPartyAuthError(err): reason = thirdPartyAuthError case messaging.IsUnavailable(err): reason = unavailable default: reason = "unknown transport error" } if me.metrics != nil { me.metrics.incrementFailure(PushNotifyAndroid, pushType, reason) } return NewErrorPushResponse(err.Error()) } if me.metrics != nil { if msg.AckID != "" { me.metrics.incrementSuccessWithAck(PushNotifyAndroid, pushType) } else { me.metrics.incrementSuccess(PushNotifyAndroid, pushType) } } return NewOkPushResponse() } func (me *AndroidNotificationServer) SendNotificationWithRetry(fcmMsg *messaging.Message) error { var err error waitTime := time.Second logger := me.logger.With(mlog.String("did", fcmMsg.Token)) // Keep a general context to make sure the whole retry // doesn't take longer than the timeout. generalContext, cancelGeneralContext := context.WithTimeout(context.Background(), me.sendTimeout) defer cancelGeneralContext() for retries := 0; retries < MAX_RETRIES; retries++ { start := time.Now() retryContext, cancelRetryContext := context.WithTimeout(generalContext, me.retryTimeout) defer cancelRetryContext() _, err = me.client.Send(retryContext, fcmMsg) if me.metrics != nil { me.metrics.observerNotificationResponse(PushNotifyAndroid, time.Since(start).Seconds()) } if err == nil || !isRetryable(err) { break } logger.Error( "Failed to send android push", mlog.Int("retry", retries), mlog.Err(err), ) if retries == MAX_RETRIES-1 { logger.Error("Max retries reached") break } select { case <-generalContext.Done(): if generalContext.Err() != nil { logger.Info( "Not retrying because context error", mlog.Int("retry", retries), mlog.Err(generalContext.Err()), ) } return generalContext.Err() case <-time.After(waitTime): } waitTime *= 2 } return err } func isRetryable(err error) bool { // We retry if the context deadline is exceeded. // This may cause double notifications, but we expect // this not to happen often. if errors.Is(err, context.DeadlineExceeded) { return true } // We retry the errors based on https://firebase.google.com/docs/cloud-messaging/http-server-ref return messaging.IsInternal(err) || messaging.IsQuotaExceeded(err) // messaging.IsUnavailable is retried by the default retry config in // firebase.google.com/go/v4@v4.14.0/internal/http_client.go // messaging.IsUnavailable(err) } func getErrorCode(err error) (string, bool) { if err == nil { return "", false } errorPointer := reflect.ValueOf(err) if errorPointer.Kind() != reflect.Ptr { return "", false } errorValue := errorPointer.Elem() if errorValue.Kind() != reflect.Struct { return "", false } codeValue := errorValue.FieldByName("ErrorCode") if !codeValue.IsValid() { return "", false } return codeValue.String(), true } ================================================ FILE: server/android_notification_test.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "encoding/json" "errors" "net/http" "os" "testing" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/shared/mlog" ) func TestAndroidInitialize(t *testing.T) { fileName := FindConfigFile("mattermost-push-proxy.sample.json") cfg, err := LoadConfig(fileName) require.NoError(t, err) logger, err := mlog.NewLogger() require.NoError(t, err) // Verify error for no service file pushSettings := AndroidPushSettings{} cfg.AndroidPushSettings[0] = pushSettings require.Error(t, NewAndroidNotificationServer(cfg.AndroidPushSettings[0], logger, nil, cfg.SendTimeoutSec, cfg.RetryTimeoutSec).Initialize()) f, err := os.CreateTemp("", "example") require.NoError(t, err) defer os.Remove(f.Name()) // clean up cfg.AndroidPushSettings[0].ServiceFileLocation = f.Name() // Verify error for bad JSON _, err = f.Write([]byte("badJSON")) require.NoError(t, err) require.Error(t, NewAndroidNotificationServer(cfg.AndroidPushSettings[0], logger, nil, cfg.SendTimeoutSec, cfg.RetryTimeoutSec).Initialize()) require.NoError(t, f.Truncate(0)) _, err = f.Seek(0, 0) require.NoError(t, err) // Verify no error for dummy JSON require.NoError(t, json.NewEncoder(f).Encode(serviceAccount{ Type: "service_account", ProjectID: "sample", })) require.NoError(t, f.Sync()) require.NoError(t, NewAndroidNotificationServer(cfg.AndroidPushSettings[0], logger, nil, cfg.SendTimeoutSec, cfg.RetryTimeoutSec).Initialize()) require.NoError(t, f.Close()) } // Copied from firebase.google.com/go/v4@v4.14.0/internal/errors.go type ErrorCode string type FirebaseError struct { ErrorCode ErrorCode String string Response *http.Response Ext map[string]interface{} } func (fe *FirebaseError) Error() string { return fe.String } func TestGetErrorCode(t *testing.T) { var errorCode ErrorCode = "some error code" err := &FirebaseError{ ErrorCode: errorCode, } extractedCode, found := getErrorCode(err) require.True(t, found) require.Equal(t, string(errorCode), extractedCode) extractedCode, found = getErrorCode(errors.New("non firebase error")) require.Equal(t, "", extractedCode) require.False(t, found) } ================================================ FILE: server/apple_notification_server.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "context" "crypto/tls" "fmt" "net/http" "net/url" "time" "github.com/kyokomi/emoji" apns "github.com/sideshow/apns2" "github.com/sideshow/apns2/certificate" "github.com/sideshow/apns2/payload" "github.com/sideshow/apns2/token" "golang.org/x/net/http2" "github.com/mattermost/mattermost/server/public/shared/mlog" ) type AppleNotificationServer struct { AppleClient *apns.Client metrics *metrics logger *mlog.Logger ApplePushSettings ApplePushSettings sendTimeout time.Duration retryTimeout time.Duration } func NewAppleNotificationServer(settings ApplePushSettings, logger *mlog.Logger, metrics *metrics, sendTimeoutSecs int, retryTimeoutSecs int) *AppleNotificationServer { return &AppleNotificationServer{ ApplePushSettings: settings, metrics: metrics, logger: logger, sendTimeout: time.Duration(sendTimeoutSecs) * time.Second, retryTimeout: time.Duration(retryTimeoutSecs) * time.Second, } } func (me *AppleNotificationServer) setupProxySettings(appleCert *tls.Certificate) error { // Override the native transport. proxyServer := getProxyServer() if proxyServer != "" { transport := &http.Transport{ Proxy: func(request *http.Request) (*url.URL, error) { return url.Parse(proxyServer) }, IdleConnTimeout: apns.HTTPClientTimeout, } if appleCert != nil { transport.TLSClientConfig = &tls.Config{ Certificates: []tls.Certificate{*appleCert}, } } err := http2.ConfigureTransport(transport) if err != nil { return fmt.Errorf("Transport Error: %v", err) } me.AppleClient.HTTPClient.Transport = transport } if appleCert != nil { me.logger.Info("Initializing apple notification server with PEM certificate", mlog.String("type", me.ApplePushSettings.Type)) } else { me.logger.Info("Initializing apple notification server with AuthKey", mlog.String("type", me.ApplePushSettings.Type)) } return nil } func (me *AppleNotificationServer) Initialize() error { if me.ApplePushSettings.AppleAuthKeyFile != "" && me.ApplePushSettings.AppleAuthKeyID != "" && me.ApplePushSettings.AppleTeamID != "" { authKey, err := token.AuthKeyFromFile(me.ApplePushSettings.AppleAuthKeyFile) if err != nil { return fmt.Errorf("Failed to initialize apple notification service with AuthKey file err=%v ", err) } appleToken := &token.Token{ AuthKey: authKey, KeyID: me.ApplePushSettings.AppleAuthKeyID, TeamID: me.ApplePushSettings.AppleTeamID, } if me.ApplePushSettings.ApplePushUseDevelopment { me.AppleClient = apns.NewTokenClient(appleToken).Development() } else { me.AppleClient = apns.NewTokenClient(appleToken).Production() } // Override the native transport. return me.setupProxySettings(nil) } if me.ApplePushSettings.ApplePushCertPrivate != "" { appleCert, appleCertErr := certificate.FromPemFile(me.ApplePushSettings.ApplePushCertPrivate, me.ApplePushSettings.ApplePushCertPassword) if appleCertErr != nil { return fmt.Errorf("Failed to initialize apple notification service with pem cert err=%v for type=%v", appleCertErr, me.ApplePushSettings.Type) } if me.ApplePushSettings.ApplePushUseDevelopment { me.AppleClient = apns.NewClient(appleCert).Development() } else { me.AppleClient = apns.NewClient(appleCert).Production() } // Override the native transport. return me.setupProxySettings(&appleCert) } return fmt.Errorf("Apple push notifications not configured. Missing ApplePushCertPrivate. for type=%v", me.ApplePushSettings.Type) } func (me *AppleNotificationServer) SendNotification(msg *PushNotification) PushResponse { data := payload.NewPayload() if msg.Badge == 0 && msg.Type == PushTypeClear && msg.AppVersion > 1 { data.Badge(1) } else if msg.Badge != -1 { data.Badge(msg.Badge) } notification := &apns.Notification{} notification.DeviceToken = msg.DeviceID notification.Payload = data notification.Topic = me.ApplePushSettings.ApplePushTopic notification.Priority = apns.PriorityHigh var pushType = msg.Type if msg.IsIDLoaded { data.Category(msg.Category) data.Sound("default") data.Custom("version", msg.Version) data.Custom("id_loaded", true) data.MutableContent() data.AlertBody(msg.Message) data.ContentAvailable() } else { switch msg.Type { case PushTypeMessage, PushTypeSession: data.Category(msg.Category) data.Sound("default") data.Custom("version", msg.Version) data.MutableContent() if msg.Type == PushTypeMessage { data.ContentAvailable() } if msg.ChannelName != "" && msg.Version == "v2" { data.AlertTitle(msg.ChannelName) data.AlertBody(emoji.Sprint(msg.Message)) data.Custom("channel_name", msg.ChannelName) } else { data.Alert(emoji.Sprint(msg.Message)) if msg.ChannelName != "" { data.Custom("channel_name", msg.ChannelName) } } case PushTypeClear, PushTypeTest: data.ContentAvailable() case PushTypeUpdateBadge: // Handled by the apps, nothing else to do here } } if me.metrics != nil { me.metrics.incrementNotificationTotal(PushNotifyApple, pushType) } data.Custom("type", pushType) data.Custom("sub_type", msg.SubType) data.Custom("server_id", msg.ServerID) if msg.AckID != "" { data.Custom("ack_id", msg.AckID) } data.Custom("is_crt_enabled", msg.IsCRTEnabled) if msg.ChannelID != "" { data.Custom("channel_id", msg.ChannelID) if msg.IsCRTEnabled && msg.RootID != "" { data.ThreadID(msg.RootID) } else { data.ThreadID(msg.ChannelID) } } if msg.Signature == "" { data.Custom("signature", "NO_SIGNATURE") } else { data.Custom("signature", msg.Signature) } if msg.TeamID != "" { data.Custom("team_id", msg.TeamID) } if msg.SenderID != "" { data.Custom("sender_id", msg.SenderID) } if msg.SenderName != "" { data.Custom("sender_name", msg.SenderName) } if msg.PostID != "" { data.Custom("post_id", msg.PostID) } if msg.RootID != "" { data.Custom("root_id", msg.RootID) } if msg.OverrideUsername != "" { data.Custom("override_username", msg.OverrideUsername) } if msg.OverrideIconURL != "" { data.Custom("override_icon_url", msg.OverrideIconURL) } if msg.FromWebhook != "" { data.Custom("from_webhook", msg.FromWebhook) } if me.AppleClient != nil { me.logger.Info( "Sending apple push notification", mlog.String("device", me.ApplePushSettings.Type), mlog.String("type", msg.Type), mlog.String("ack_id", msg.AckID), ) res, err := me.SendNotificationWithRetry(notification) if err != nil { me.logger.Error( "Failed to send apple push", mlog.String("sid", msg.ServerID), mlog.String("did", msg.DeviceID), mlog.Err(err), mlog.String("type", me.ApplePushSettings.Type), ) if me.metrics != nil { me.metrics.incrementFailure(PushNotifyApple, pushType, "RequestError") } return NewErrorPushResponse("unknown transport error") } if !res.Sent() { if res.Reason == apns.ReasonBadDeviceToken || res.Reason == apns.ReasonUnregistered || res.Reason == apns.ReasonMissingDeviceToken || res.Reason == apns.ReasonDeviceTokenNotForTopic { me.logger.Info( "Failed to send apple push sending remove code res", mlog.String("ApnsID", res.ApnsID), mlog.String("reason", res.Reason), mlog.Int("code", res.StatusCode), mlog.String("type", me.ApplePushSettings.Type), ) if me.metrics != nil { me.metrics.incrementRemoval(PushNotifyApple, pushType, res.Reason) } return NewRemovePushResponse() } me.logger.Error( "Failed to send apple push with res", mlog.String("ApnsID", res.ApnsID), mlog.String("reason", res.Reason), mlog.Int("code", res.StatusCode), mlog.String("type", me.ApplePushSettings.Type), ) if me.metrics != nil { me.metrics.incrementFailure(PushNotifyApple, pushType, res.Reason) } return NewErrorPushResponse("unknown send response error") } } if me.metrics != nil { if msg.AckID != "" { me.metrics.incrementSuccessWithAck(PushNotifyApple, pushType) } else { me.metrics.incrementSuccess(PushNotifyApple, pushType) } } return NewOkPushResponse() } func (me *AppleNotificationServer) SendNotificationWithRetry(notification *apns.Notification) (*apns.Response, error) { var res *apns.Response var err error waitTime := time.Second // Keep a general context to make sure the whole retry // doesn't take longer than the timeout. generalContext, cancelGeneralContext := context.WithTimeout(context.Background(), me.sendTimeout) defer cancelGeneralContext() for retries := 0; retries < MAX_RETRIES; retries++ { start := time.Now() retryContext, cancelRetryContext := context.WithTimeout(generalContext, me.retryTimeout) defer cancelRetryContext() res, err = me.AppleClient.PushWithContext(retryContext, notification) if me.metrics != nil { me.metrics.observerNotificationResponse(PushNotifyApple, time.Since(start).Seconds()) } if err == nil { break } me.logger.Error( "Failed to send apple push", mlog.String("did", notification.DeviceToken), mlog.Int("retry", retries), mlog.Err(err), ) if retries == MAX_RETRIES-1 { me.logger.Error("Max retries reached", mlog.String("did", notification.DeviceToken)) break } select { case <-generalContext.Done(): case <-time.After(waitTime): } if generalContext.Err() != nil { me.logger.Info( "Not retrying because context error", mlog.String("did", notification.DeviceToken), mlog.Int("retry", retries), mlog.Err(generalContext.Err()), ) err = generalContext.Err() break } waitTime *= 2 } return res, err } ================================================ FILE: server/config_push_proxy.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "encoding/json" "fmt" "io" "os" "path/filepath" ) type ConfigPushProxy struct { AndroidPushSettings []AndroidPushSettings ListenAddress string ThrottleVaryByHeader string LogFileLocation string SendTimeoutSec int RetryTimeoutSec int ApplePushSettings []ApplePushSettings EnableMetrics bool EnableConsoleLog bool EnableFileLog bool LogFormat string // json or plain ThrottlePerSec int ThrottleMemoryStoreSize int } type ApplePushSettings struct { Type string ApplePushCertPrivate string ApplePushCertPassword string ApplePushTopic string AppleAuthKeyFile string AppleAuthKeyID string AppleTeamID string ApplePushUseDevelopment bool } type AndroidPushSettings struct { Type string AndroidAPIKey string `json:"AndroidApiKey"` ServiceFileLocation string `json:"ServiceFileLocation"` } // FindConfigFile searches for the filepath in a list of directories // and then returns the absolute path to that file. func FindConfigFile(fileName string) string { if _, err := os.Stat("/tmp/" + fileName); err == nil { fileName, _ = filepath.Abs("/tmp/" + fileName) } else if _, err := os.Stat("./config/" + fileName); err == nil { fileName, _ = filepath.Abs("./config/" + fileName) } else if _, err := os.Stat("../config/" + fileName); err == nil { fileName, _ = filepath.Abs("../config/" + fileName) } else if _, err := os.Stat(fileName); err == nil { fileName, _ = filepath.Abs(fileName) } return fileName } // LoadConfig loads the config from the given file path. func LoadConfig(fileName string) (*ConfigPushProxy, error) { file, err := os.Open(fileName) if err != nil { return nil, err } defer file.Close() buf, err := io.ReadAll(file) if err != nil { return nil, err } var cfg *ConfigPushProxy err = json.Unmarshal(buf, &cfg) if err != nil { fmt.Println(buf, err) return nil, err } if !cfg.EnableConsoleLog && !cfg.EnableFileLog { cfg.EnableConsoleLog = true } // Set timeout defaults if cfg.SendTimeoutSec == 0 { cfg.SendTimeoutSec = 30 } if cfg.RetryTimeoutSec == 0 { cfg.RetryTimeoutSec = 8 } if cfg.RetryTimeoutSec > cfg.SendTimeoutSec { cfg.RetryTimeoutSec = cfg.SendTimeoutSec } if cfg.EnableFileLog { if cfg.LogFileLocation == "" { // We just do an mkdir -p equivalent. // Otherwise, it would need 2 steps of statting and creating. err := os.MkdirAll("./logs", 0755) if err != nil { // If it fails, we log in the current directory itself cfg.LogFileLocation = "./push_proxy.log" } else { cfg.LogFileLocation = "./logs/push_proxy.log" } } // if file does not exist, create it. if _, err := os.Stat(cfg.LogFileLocation); os.IsNotExist(err) { f, err := os.Create(cfg.LogFileLocation) if err != nil { return nil, err } if err := f.Close(); err != nil { return nil, err } } } return cfg, nil } ================================================ FILE: server/logger.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "encoding/json" "github.com/mattermost/mattermost/server/public/shared/mlog" ) func NewLogger(cfg *ConfigPushProxy) (*mlog.Logger, error) { // Initialize the logger - begin logger, err := mlog.NewLogger() if err != nil { return nil, err } if cfg.LogFormat != "plain" && cfg.LogFormat != "json" { cfg.LogFormat = "plain" } err = logger.ConfigureTargets(buildLogConfig(cfg), nil) if err != nil { return logger, err } return logger, nil } func buildLogConfig(cfg *ConfigPushProxy) mlog.LoggerConfiguration { logConf := make(mlog.LoggerConfiguration) if cfg.EnableFileLog && cfg.LogFileLocation != "" { logConf["file"] = buildLogFileConfig(cfg.LogFileLocation, cfg.LogFormat) } if cfg.EnableConsoleLog || cfg.LogFileLocation == "" || !cfg.EnableFileLog { logConf["console"] = buildConsoleLogConfig(cfg.LogFormat) } return logConf } func buildConsoleLogConfig(format string) mlog.TargetCfg { return mlog.TargetCfg{ Type: "console", Levels: mlog.StdAll, Format: format, Options: json.RawMessage(`{"out": "stdout"}`), FormatOptions: json.RawMessage(`{"enable_color": true, "enable_caller": true}`), MaxQueueSize: 1000, } } func buildLogFileConfig(filename string, format string) mlog.TargetCfg { opts := struct { Filename string `json:"filename"` Max_size int `json:"max_size"` Max_age int `json:"max_age"` Max_backups int `json:"max_backups"` Compress bool `json:"compress"` }{ Filename: filename, Max_size: 100, Max_age: 0, Max_backups: 0, Compress: true, } var optsJsonString, _ = json.Marshal(opts) return mlog.TargetCfg{ Type: "file", Levels: mlog.StdAll, Format: format, Options: optsJsonString, FormatOptions: json.RawMessage(`{"enable_color": true, "enable_caller": true}`), MaxQueueSize: 1000, } } ================================================ FILE: server/logger_test.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "os" "testing" ) func TestNewMlogLogger(t *testing.T) { t.Run("Instancing logger with implicit plain console", func(t *testing.T) { cfg := &ConfigPushProxy{ EnableFileLog: false, } logger, err := NewLogger(cfg) assert.NoError(t, err) assert.NotNil(t, logger) }) t.Run("Instancing logger with json file", func(t *testing.T) { log, err := os.CreateTemp("", "log") require.NoError(t, err) err = log.Close() require.NoError(t, err) defer os.Remove(log.Name()) cfg := &ConfigPushProxy{ EnableFileLog: true, LogFileLocation: log.Name(), LogFormat: "json", } logger, err := NewLogger(cfg) assert.NoError(t, err) assert.NotNil(t, logger) }) t.Run("Instancing logger with both file and console", func(t *testing.T) { log, err := os.CreateTemp("", "log") require.NoError(t, err) err = log.Close() require.NoError(t, err) defer os.Remove(log.Name()) cfg := &ConfigPushProxy{ EnableConsoleLog: true, EnableFileLog: true, LogFileLocation: log.Name(), LogFormat: "json", } logger, err := NewLogger(cfg) assert.NoError(t, err) assert.NotNil(t, logger) }) } ================================================ FILE: server/metrics.go ================================================ package server import ( "net/http" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) const ( metricNotificationsTotalName = "service_notifications_total" metricSuccessName = "service_success_total" metricSuccessWithAckName = "service_success_with_ack_total" metricDeliveredName = "service_delivered_total" metricFailureName = "service_failure_total" metricFailureWithReasonName = "service_failure_with_reason_total" metricRemovalName = "service_removal_total" metricBadRequestName = "service_bad_request_total" metricFCMResponseName = "service_fcm_request_duration_seconds" metricAPNSResponseName = "service_apns_request_duration_seconds" metricServiceResponseName = "service_request_duration_seconds" metricNotificationResponseName = "service_notification_duration_seconds" ) // NewPrometheusHandler returns the http.Handler to expose Prometheus metrics func NewPrometheusHandler() http.Handler { return promhttp.Handler() } type metrics struct { metricNotificationsTotal *prometheus.CounterVec metricSuccess *prometheus.CounterVec metricSuccessWithAck *prometheus.CounterVec metricDelivered *prometheus.CounterVec metricFailure *prometheus.CounterVec metricFailureWithReason *prometheus.CounterVec metricRemoval *prometheus.CounterVec metricBadRequest prometheus.Counter metricAPNSResponse prometheus.Histogram metricFCMResponse prometheus.Histogram metricNotificationResponse *prometheus.HistogramVec metricServiceResponse prometheus.Histogram } // newMetrics initializes the metrics and registers them func newMetrics() *metrics { m := &metrics{ metricNotificationsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: metricNotificationsTotalName, Help: "Number of notifications sent"}, []string{"platform", "type"}), metricSuccess: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: metricSuccessName, Help: "Number of push success."}, []string{"platform", "type"}), metricSuccessWithAck: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: metricSuccessWithAckName, Help: "Number of push success that contains ackId."}, []string{"platform", "type"}, ), metricDelivered: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: metricDeliveredName, Help: "Number of push delivered."}, []string{"platform", "type"}, ), metricFailure: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: metricFailureName, Help: "Number of push errors."}, []string{"platform", "type"}), metricFailureWithReason: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: metricFailureWithReasonName, Help: "Number of push errors with reasons."}, []string{"platform", "type", "reason"}), metricRemoval: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: metricRemovalName, Help: "Number of device token errors."}, []string{"platform", "reason"}), metricBadRequest: prometheus.NewCounter(prometheus.CounterOpts{ Name: metricBadRequestName, Help: "Request to push proxy was a bad request", }), metricAPNSResponse: prometheus.NewHistogram(prometheus.HistogramOpts{ Name: metricAPNSResponseName, Help: "Request latency distribution", }), metricFCMResponse: prometheus.NewHistogram(prometheus.HistogramOpts{ Name: metricFCMResponseName, Help: "Request latency distribution", }), metricNotificationResponse: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: metricNotificationResponseName, Help: "Notifiction request latency distribution"}, []string{"platform"}), metricServiceResponse: prometheus.NewHistogram(prometheus.HistogramOpts{ Name: metricServiceResponseName, Help: "Request latency distribution", }), } prometheus.MustRegister( m.metricNotificationsTotal, m.metricSuccess, m.metricSuccessWithAck, m.metricFailure, m.metricFailureWithReason, m.metricRemoval, m.metricBadRequest, m.metricAPNSResponse, m.metricFCMResponse, m.metricServiceResponse, m.metricNotificationResponse, ) return m } func (m *metrics) shutdown() { func(cs ...prometheus.Collector) { for _, c := range cs { prometheus.Unregister(c) } }( m.metricNotificationsTotal, m.metricSuccess, m.metricSuccessWithAck, m.metricFailure, m.metricFailureWithReason, m.metricRemoval, m.metricBadRequest, m.metricAPNSResponse, m.metricFCMResponse, m.metricServiceResponse, m.metricNotificationResponse, ) } func (m *metrics) incrementNotificationTotal(platform, pushType string) { m.metricNotificationsTotal.WithLabelValues(platform, pushType).Inc() } func (m *metrics) incrementSuccess(platform, pushType string) { m.metricSuccess.WithLabelValues(platform, pushType).Inc() } func (m *metrics) incrementSuccessWithAck(platform, pushType string) { m.incrementSuccess(platform, pushType) m.metricSuccessWithAck.WithLabelValues(platform, pushType).Inc() } func (m *metrics) incrementDelivered(platform, pushType string) { m.metricDelivered.WithLabelValues(platform, pushType).Inc() } func (m *metrics) incrementFailure(platform, pushType, reason string) { m.metricFailure.WithLabelValues(platform, pushType).Inc() if reason != "" { m.metricFailureWithReason.WithLabelValues(platform, pushType, reason).Inc() } } func (m *metrics) incrementRemoval(platform, pushType, reason string) { m.metricRemoval.WithLabelValues(platform, reason).Inc() m.incrementFailure(platform, pushType, reason) } func (m *metrics) incrementBadRequest() { m.metricBadRequest.Inc() } func (m *metrics) observeAPNSResponse(dur float64) { m.metricAPNSResponse.Observe(dur) } func (m *metrics) observeFCMResponse(dur float64) { m.metricFCMResponse.Observe(dur) } func (m *metrics) observeServiceResponse(dur float64) { m.metricServiceResponse.Observe(dur) } func (m *metrics) observerNotificationResponse(platform string, dur float64) { m.metricNotificationResponse.WithLabelValues(platform).Observe(dur) switch platform { case PushNotifyApple: m.observeAPNSResponse(dur) case PushNotifyAndroid: m.observeFCMResponse(dur) } } ================================================ FILE: server/metrics_test.go ================================================ package server import ( "io" "net/http" "strings" "testing" "time" "github.com/prometheus/common/expfmt" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/shared/mlog" ) func TestMetricDisabled(t *testing.T) { t.Log("Testing Metrics Enabled") platform := "junk" pushType := PushTypeMessage fileName := FindConfigFile("mattermost-push-proxy.sample.json") cfg, err := LoadConfig(fileName) require.NoError(t, err) cfg.AndroidPushSettings[0].AndroidAPIKey = platform cfg.EnableMetrics = false logger, err := mlog.NewLogger() require.NoError(t, err) srv := New(cfg, logger) srv.Start() time.Sleep(time.Second * 2) defer func() { srv.Stop() time.Sleep(time.Second * 2) }() m := newMetrics() defer m.shutdown() m.incrementBadRequest() m.incrementNotificationTotal(platform, pushType) m.incrementSuccess(platform, pushType) m.incrementRemoval(platform, pushType, "not registered") m.incrementFailure(platform, pushType, "error") m.observerNotificationResponse(PushNotifyApple, 1) m.observerNotificationResponse(PushNotifyAndroid, 1) m.observeServiceResponse(1) resp, err := http.Get("http://localhost:8066/metrics") if err != nil { t.Fatalf("service should not return an http error") } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("service should return a parsable response") } if !strings.Contains(string(data), "404 page not found") { t.Fatalf("service should return a 404") } } func TestMetricEnabled(t *testing.T) { t.Log("Testing Metrics Enabled") platform := "junk" pushType := PushTypeMessage fileName := FindConfigFile("mattermost-push-proxy.sample.json") cfg, err := LoadConfig(fileName) require.NoError(t, err) cfg.AndroidPushSettings[0].AndroidAPIKey = platform cfg.EnableMetrics = true logger, err := mlog.NewLogger() require.NoError(t, err) srv := New(cfg, logger) srv.Start() time.Sleep(time.Second * 2) defer func() { srv.Stop() time.Sleep(time.Second * 2) }() srv.metrics.incrementBadRequest() srv.metrics.incrementNotificationTotal(platform, pushType) srv.metrics.incrementSuccess(platform, pushType) srv.metrics.incrementRemoval(platform, pushType, "not registered") srv.metrics.incrementFailure(platform, pushType, "error") srv.metrics.observerNotificationResponse(PushNotifyApple, 1) srv.metrics.observerNotificationResponse(PushNotifyAndroid, 1) srv.metrics.observeServiceResponse(1) resp, err := http.Get("http://localhost:8066/metrics") if err != nil { t.Fatalf("failed to get metrics endpoint - %s", err.Error()) } defer resp.Body.Close() parser := &expfmt.TextParser{} metrics, _ := parser.TextToMetricFamilies(resp.Body) counters := []string{metricSuccessName, metricFailureName, metricFailureWithReasonName, metricRemovalName, metricBadRequestName, metricNotificationsTotalName} for _, cn := range counters { if m, ok := metrics[cn]; !ok { t.Fatalf("metric not found. name: %s", cn) } else { val := m.Metric[0].Counter.Value result := float64(1) if cn == metricFailureName { result = float64(2) } if val == nil { t.Fatalf("no metric value. name: %s", cn) } if *val != result { t.Fatalf("metric value does not match. mame: %s, got: %v, expected: %v", cn, *val, result) } } } histograms := []string{metricAPNSResponseName, metricFCMResponseName, metricServiceResponseName, metricNotificationResponseName} for _, hn := range histograms { if m, ok := metrics[hn]; !ok { t.Fatalf("metric not found. name: %s", hn) } else { val := m.Metric[0].Histogram.SampleCount if val == nil { t.Fatalf("no metric value. name: %s", hn) } if *val != 1 { t.Fatalf("metric value does not match. mame: %s, got: %v, expected: %v", hn, *val, 1) } } } } ================================================ FILE: server/push_notification.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server const ( PushNotifyApple = "apple" PushNotifyAndroid = "android" PushTypeMessage = "message" PushTypeClear = "clear" PushTypeUpdateBadge = "update_badge" PushTypeSession = "session" PushTypeTest = "test" PushMessageV2 = "v2" PushSoundNone = "none" ) type PushNotificationAck struct { ID string `json:"id"` Platform string `json:"platform"` Type string `json:"type"` } type PushNotification struct { ID string `json:"id"` AckID string `json:"ack_id"` Platform string `json:"platform"` ServerID string `json:"server_id"` DeviceID string `json:"device_id"` Category string `json:"category"` Sound string `json:"sound"` Message string `json:"message"` TeamID string `json:"team_id"` ChannelID string `json:"channel_id"` PostID string `json:"post_id"` RootID string `json:"root_id"` ChannelName string `json:"channel_name"` Type string `json:"type"` SubType string `json:"sub_type,omitempty"` SenderName string `json:"sender_name"` SenderID string `json:"sender_id"` OverrideUsername string `json:"override_username"` OverrideIconURL string `json:"override_icon_url"` FromWebhook string `json:"from_webhook"` Version string `json:"version"` AppVersion int `json:"app_version,omitempty"` Badge int `json:"badge"` ContentAvailable int `json:"cont_ava"` IsCRTEnabled bool `json:"is_crt_enabled"` IsIDLoaded bool `json:"is_id_loaded"` Signature string `json:"signature"` } ================================================ FILE: server/push_response.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "encoding/json" "io" ) const ( PUSH_STATUS = "status" PUSH_STATUS_OK = "OK" PUSH_STATUS_FAIL = "FAIL" PUSH_STATUS_REMOVE = "REMOVE" PUSH_STATUS_ERROR_MSG = "error" ) type PushResponse map[string]string func NewOkPushResponse() PushResponse { m := make(map[string]string) m[PUSH_STATUS] = PUSH_STATUS_OK return m } func NewRemovePushResponse() PushResponse { m := make(map[string]string) m[PUSH_STATUS] = PUSH_STATUS_REMOVE return m } func NewErrorPushResponse(message string) PushResponse { m := make(map[string]string) m[PUSH_STATUS] = PUSH_STATUS_FAIL m[PUSH_STATUS_ERROR_MSG] = message return m } func PushResponseFromJson(data io.Reader) PushResponse { decoder := json.NewDecoder(data) var objmap PushResponse if err := decoder.Decode(&objmap); err != nil { return make(map[string]string) } else { return objmap } } ================================================ FILE: server/server.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "context" "encoding/json" "fmt" "net" "net/http" "os" "strconv" "strings" "time" "github.com/gorilla/handlers" "github.com/gorilla/mux" throttled "gopkg.in/throttled/throttled.v1" throttledStore "gopkg.in/throttled/throttled.v1/store" "github.com/mattermost/mattermost-push-proxy/internal/version" "github.com/mattermost/mattermost/server/public/shared/mlog" ) const ( HEADER_FORWARDED = "X-Forwarded-For" HEADER_REAL_IP = "X-Real-IP" WAIT_FOR_SERVER_SHUTDOWN = time.Second * 5 CONNECTION_TIMEOUT_SECONDS = 60 MAX_RETRIES = 3 ) type NotificationServer interface { SendNotification(msg *PushNotification) PushResponse Initialize() error } // Server is the main struct which performs all activities. type Server struct { cfg *ConfigPushProxy httpServer *http.Server pushTargets map[string]NotificationServer metrics *metrics logger *mlog.Logger } // New returns a new Server instance. func New(cfg *ConfigPushProxy, logger *mlog.Logger) *Server { return &Server{ cfg: cfg, pushTargets: make(map[string]NotificationServer), logger: logger, } } // Start starts the server. func (s *Server) Start() { v := version.VersionInfo() s.logger.Info("Push proxy server is initializing...", mlog.String("version", v.String())) proxyServer := getProxyServer() if proxyServer != "" { s.logger.Info("Proxy server detected.", mlog.String("proxyServer", proxyServer)) } var m *metrics if s.cfg.EnableMetrics { m = newMetrics() s.metrics = m } for _, settings := range s.cfg.ApplePushSettings { server := NewAppleNotificationServer(settings, s.logger, m, s.cfg.SendTimeoutSec, s.cfg.RetryTimeoutSec) err := server.Initialize() if err != nil { s.logger.Error("Failed to initialize client", mlog.Err(err)) continue } s.pushTargets[settings.Type] = server } for _, settings := range s.cfg.AndroidPushSettings { server := NewAndroidNotificationServer(settings, s.logger, m, s.cfg.SendTimeoutSec, s.cfg.RetryTimeoutSec) err := server.Initialize() if err != nil { s.logger.Error("Failed to initialize client", mlog.Err(err)) continue } s.pushTargets[settings.Type] = server } router := mux.NewRouter() vary := throttled.VaryBy{} vary.RemoteAddr = false vary.Headers = strings.Fields(s.cfg.ThrottleVaryByHeader) th := throttled.RateLimit(throttled.PerSec(s.cfg.ThrottlePerSec), &vary, throttledStore.NewMemStore(s.cfg.ThrottleMemoryStoreSize)) th.DeniedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.logger.Error("Error: code=429", mlog.String("path", r.URL.Path), mlog.String("ip", s.getIpAddress(r))) throttled.DefaultDeniedHandler.ServeHTTP(w, r) }) handler := th.Throttle(router) router.HandleFunc("/", root).Methods("GET") router.HandleFunc("/version", s.version).Methods("GET") metricCompatibleSendNotificationHandler := s.handleSendNotification metricCompatibleAckNotificationHandler := s.handleAckNotification if s.cfg.EnableMetrics { metrics := NewPrometheusHandler() router.Handle("/metrics", metrics).Methods("GET") metricCompatibleSendNotificationHandler = s.responseTimeMiddleware(s.handleSendNotification) metricCompatibleAckNotificationHandler = s.responseTimeMiddleware(s.handleAckNotification) } r := router.PathPrefix("/api/v1").Subrouter() r.HandleFunc("/send_push", metricCompatibleSendNotificationHandler).Methods("POST") r.HandleFunc("/ack", metricCompatibleAckNotificationHandler).Methods("POST") s.httpServer = &http.Server{ Addr: s.cfg.ListenAddress, Handler: handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler), ReadTimeout: time.Duration(CONNECTION_TIMEOUT_SECONDS) * time.Second, WriteTimeout: time.Duration(CONNECTION_TIMEOUT_SECONDS) * time.Second, } go func() { err := s.httpServer.ListenAndServe() if err != http.ErrServerClosed { s.logger.Fatal(err.Error()) } }() s.logger.Info("Server is listening on " + s.cfg.ListenAddress) } // Stop stops the server. func (s *Server) Stop() { s.logger.Info("Stopping Server...") ctx, cancel := context.WithTimeout(context.Background(), WAIT_FOR_SERVER_SHUTDOWN) defer cancel() if s.metrics != nil { s.metrics.shutdown() } // Close shop err := s.httpServer.Shutdown(ctx) if err != nil { s.logger.Error(err.Error()) } } func root(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("Mattermost Push Proxy")) } func (s *Server) version(w http.ResponseWriter, _ *http.Request) { info := version.VersionInfo() w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(info); err != nil { s.logger.Error("Failed to write response", mlog.Err(err)) if s.metrics != nil { s.metrics.incrementBadRequest() } } } func (s *Server) responseTimeMiddleware(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { start := time.Now() f(w, r) if s.metrics != nil { s.metrics.observeServiceResponse(time.Since(start).Seconds()) } } } func (s *Server) handleSendNotification(w http.ResponseWriter, r *http.Request) { var msg PushNotification err := json.NewDecoder(r.Body).Decode(&msg) if err != nil { rMsg := fmt.Sprintf("Failed to read message body: %v", err) s.logger.Error(rMsg) resp := NewErrorPushResponse(rMsg) if err2 := json.NewEncoder(w).Encode(resp); err2 != nil { s.logger.Error("Failed to write response", mlog.Err(err2)) } if s.metrics != nil { s.metrics.incrementBadRequest() } return } if msg.ServerID == "" { rMsg := "Failed because of missing server Id" s.logger.Error(rMsg) resp := NewErrorPushResponse(rMsg) if err2 := json.NewEncoder(w).Encode(resp); err2 != nil { s.logger.Error("Failed to write response", mlog.Err(err2)) } if s.metrics != nil { s.metrics.incrementBadRequest() } return } if msg.DeviceID == "" { rMsg := fmt.Sprintf("Failed because of missing device Id serverId=%v", msg.ServerID) s.logger.Error(rMsg) resp := NewErrorPushResponse(rMsg) if err2 := json.NewEncoder(w).Encode(resp); err2 != nil { s.logger.Error("Failed to write response", mlog.Err(err2)) } if s.metrics != nil { s.metrics.incrementBadRequest() } return } if len(msg.Message) > 2047 { msg.Message = msg.Message[0:2046] } if len(msg.ChannelName) > 64 { msg.ChannelName = msg.ChannelName[0:64] } // Parse the app version if available index := strings.Index(msg.Platform, "-v") platform := msg.Platform msg.AppVersion = 1 if index > -1 { msg.Platform = platform[:index] appVersionString := platform[index+2:] version, e := strconv.Atoi(appVersionString) if e == nil { msg.AppVersion = version } else { rMsg := fmt.Sprintf("Could not determine the app version in %v appVersion=%v", msg.Platform, appVersionString) s.logger.Error(rMsg) } } if server, ok := s.pushTargets[msg.Platform]; ok { rMsg := server.SendNotification(&msg) if err2 := json.NewEncoder(w).Encode(rMsg); err2 != nil { s.logger.Error("Failed to write message", mlog.Err(err2)) } return } rMsg := fmt.Sprintf("Did not send message because of missing platform property type=%v serverId=%v", msg.Platform, msg.ServerID) s.logger.Error(rMsg) resp := NewErrorPushResponse(rMsg) err = json.NewEncoder(w).Encode(resp) if err != nil { s.logger.Error("Failed to write response", mlog.Err(err)) } if s.metrics != nil { s.metrics.incrementBadRequest() } } func (s *Server) handleAckNotification(w http.ResponseWriter, r *http.Request) { var ack PushNotificationAck err := json.NewDecoder(r.Body).Decode(&ack) if err != nil { msg := fmt.Sprintf("Failed to read ack body: %v", err) s.logger.Error(msg) resp := NewErrorPushResponse(msg) if err2 := json.NewEncoder(w).Encode(resp); err2 != nil { s.logger.Error("Failed to write response", mlog.Err(err2)) } if s.metrics != nil { s.metrics.incrementBadRequest() } return } if ack.ID == "" { msg := "Failed because of missing ack Id" s.logger.Error(msg) resp := NewErrorPushResponse(msg) if err := json.NewEncoder(w).Encode(resp); err != nil { s.logger.Error("Failed to write response", mlog.Err(err)) } if s.metrics != nil { s.metrics.incrementBadRequest() } return } if ack.Platform == "" { msg := "Failed because of missing ack platform" s.logger.Error(msg) resp := NewErrorPushResponse(msg) if err := json.NewEncoder(w).Encode(resp); err != nil { s.logger.Error("Failed to write response", mlog.Err(err)) } if s.metrics != nil { s.metrics.incrementBadRequest() } return } if ack.Type == "" { msg := "Failed because of missing ack type" s.logger.Error(msg) resp := NewErrorPushResponse(msg) if err := json.NewEncoder(w).Encode(resp); err != nil { s.logger.Error("Failed to write response", mlog.Err(err)) } if s.metrics != nil { s.metrics.incrementBadRequest() } return } // Increment ACK s.logger.Info("Acknowledged delivery receipt", mlog.String("ack_id", ack.ID)) if s.metrics != nil { s.metrics.incrementDelivered(ack.Platform, ack.Type) } rMsg := NewOkPushResponse() if err := json.NewEncoder(w).Encode(rMsg); err != nil { s.logger.Error("Failed to write message", mlog.Err(err)) } } func (s *Server) getIpAddress(r *http.Request) string { address := r.Header.Get(HEADER_FORWARDED) var err error if address == "" { address = r.Header.Get(HEADER_REAL_IP) } if address == "" { address, _, err = net.SplitHostPort(r.RemoteAddr) if err != nil { s.logger.Error("error in getting IP address", mlog.Err(err)) } } return address } func getProxyServer() string { // HTTPS_PROXY gets the higher priority. proxyServer := os.Getenv("HTTPS_PROXY") if proxyServer == "" { proxyServer = os.Getenv("HTTP_PROXY") } return proxyServer } ================================================ FILE: server/server_test.go ================================================ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. package server import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/mattermost/mattermost-push-proxy/internal/version" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBasicServer(t *testing.T) { fileName := FindConfigFile("mattermost-push-proxy.sample.json") cfg, err := LoadConfig(fileName) require.NoError(t, err) logger, err := mlog.NewLogger() require.NoError(t, err) srv := New(cfg, logger) srv.Start() time.Sleep(time.Second * 2) msg := PushNotification{} msg.Message = "test" msg.Badge = 1 msg.DeviceID = "test" // Test for missing server Id client := http.Client{} buf, err := json.Marshal(msg) require.NoError(t, err) rq, _ := http.NewRequest("POST", "http://localhost:8066/api/v1/send_push", bytes.NewReader(buf)) if resp, err2 := client.Do(rq); err2 != nil { t.Fatal(err2) } else { pr := PushResponseFromJson(resp.Body) if pr == nil || pr[PUSH_STATUS] != PUSH_STATUS_FAIL { t.Fatal("invalid response") } } // Test for missing platform type msg.ServerID = "test" client = http.Client{} buf, err = json.Marshal(msg) require.NoError(t, err) rq, _ = http.NewRequest("POST", "http://localhost:8066/api/v1/send_push", bytes.NewReader(buf)) if resp, err2 := client.Do(rq); err2 != nil { t.Fatal(err2) } else { pr := PushResponseFromJson(resp.Body) if pr == nil || pr[PUSH_STATUS] != PUSH_STATUS_FAIL { t.Fatal("invalid response") } } // Test for junk platform type msg.Platform = "junk" buf, err = json.Marshal(msg) require.NoError(t, err) rq, _ = http.NewRequest("POST", "http://localhost:8066/api/v1/send_push", bytes.NewReader(buf)) if resp, err := client.Do(rq); err != nil { t.Fatal(err) } else { pr := PushResponseFromJson(resp.Body) if pr == nil || pr[PUSH_STATUS] != PUSH_STATUS_FAIL { t.Fatal("invalid response") } } srv.Stop() time.Sleep(time.Second * 2) } func TestAndroidSend(t *testing.T) { fileName := FindConfigFile("mattermost-push-proxy.sample.json") cfg, err := LoadConfig(fileName) require.NoError(t, err) cfg.AndroidPushSettings[0].AndroidAPIKey = "junk" logger, err := mlog.NewLogger() require.NoError(t, err) srv := New(cfg, logger) srv.Start() time.Sleep(time.Second * 2) msg := PushNotification{} msg.Message = "test" msg.Badge = 1 msg.Platform = PushNotifyAndroid msg.ServerID = "test" msg.DeviceID = "test" client := http.Client{} buf, err := json.Marshal(msg) require.NoError(t, err) rq, _ := http.NewRequest("POST", "http://localhost:8066/api/v1/send_push", bytes.NewReader(buf)) if resp, err := client.Do(rq); err != nil { t.Fatal(err) } else { pr := PushResponseFromJson(resp.Body) if pr == nil || pr[PUSH_STATUS] != PUSH_STATUS_FAIL { t.Fatal("invalid response") } } srv.Stop() time.Sleep(time.Second * 2) } func TestServer_version(t *testing.T) { fileName := FindConfigFile("mattermost-push-proxy.sample.json") cfg, err := LoadConfig(fileName) require.NoError(t, err) logger, err := mlog.NewLogger() require.NoError(t, err) srv := New(cfg, logger) req := httptest.NewRequest(http.MethodGet, "/version", nil) res := httptest.NewRecorder() srv.version(res, req) assert.Equal(t, res.Code, http.StatusOK) info := version.VersionInfo() ret := struct { Version string Hash string }{} err = json.NewDecoder(res.Body).Decode(&ret) assert.NoError(t, err) assert.Equal(t, info.BuildVersion, ret.Version) assert.Equal(t, info.BuildHash, ret.Hash) } ================================================ FILE: swagger/spec.yaml ================================================ openapi: 3.0.1 info: title: Mattermost Push Proxy description: 'This is the OpenAPI documentation for Mattermost Push Proxy REST API' version: 1.0.0 servers: - url: http://url-to-push-proxy.com/api/v1 paths: /send_push: post: summary: Send push notification requestBody: description: Push notification request body content: '*/*': schema: $ref: '#/components/schemas/PushNotification' example: type: "update_badge" device_id: "ackljrfoegdflghdg" platform: "android" ack_id: "kfs095jsdsdfjslj" version: "v2" sound: "none" cont_ava: 1 badge: 2 required: true responses: default: description: response content: application/json: schema: oneOf: - $ref: '#/components/schemas/PushResponseOK' - $ref: '#/components/schemas/PushResponseRemove' - $ref: '#/components/schemas/PushResponseError' example: status: OK /ack: post: summary: Send acknowledgement of a push notification requestBody: description: Send acknowledgement request body content: '*/*': schema: $ref: '#/components/schemas/PushNotificationAck' example: id: "abcxyz" platform: "android" type: "message" required: true responses: default: description: response content: application/json: schema: oneOf: - $ref: '#/components/schemas/PushResponseOK' - $ref: '#/components/schemas/PushResponseError' example: status: OK components: schemas: PushNotification: type: object properties: ack_id: type: string description: "id of the acknowledgement" platform: type: string description: "type of the platform" enum: - apple - android - apple_rn - android_rn server_id: type: string description: "id of the server" device_id: type: string description: "id of the device" category: type: string description: "category of the message" sound: type: string description: "type of sound to make when message arrives" message: type: string description: "contents of the message" badge: type: integer description: "badge count" format: int64 cont_ava: description: "content available" type: integer format: int64 team_id: description: "id of the team" type: string channel_id: description: "id of the channel" type: string root_id: description: "id of the root of the post" type: string post_id: description: "id of the post" type: string channel_name: description: "name of the channel" type: string type: type: string description: "type of the message" enum: - message - clear - update_badge - session sender_name: description: "name of the sender" type: string sender_id: description: "id of the sender" type: string override_username: description: "override the default username" type: string override_icon_url: description: "override the default icon url" type: string from_webhook: description: "name of the webhook" type: string version: type: string description: "version of the message" default: "v2" is_id_loaded: description: "whether the message is id_loaded or not" type: boolean PushNotificationAck: type: object properties: id: type: string description: "id of the acknowledgement" platform: type: string description: "type of the platform" enum: - apple - android - apple_rn - android_rn type: type: string description: "type of the message" enum: - message - clear - update_badge - session PushResponseOK: type: object properties: status: type: string default: "OK" PushResponseRemove: type: object properties: status: type: string default: "REMOVE" PushResponseError: type: object properties: status: type: string default: "FAIL" error: type: string ================================================ FILE: swagger/template.hbs ================================================ {{title}} {{{redocHead}}} {{#unless disableGoogleFont}}{{/unless}} {{{redocHTML}}}