├── .dockerignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── codeql.yaml │ ├── docker.yaml │ ├── goreleaser.yml │ └── testing.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .hadolint.yaml ├── LICENSE ├── Makefile ├── README.md ├── certificate ├── authkey-invalid.p8 ├── authkey-valid.p8 ├── certificate-valid.p12 ├── certificate-valid.pem ├── localhost.cert └── localhost.key ├── config ├── config.go ├── config_test.go └── testdata │ ├── config.yml │ └── empty.yml ├── contrib └── init │ └── debian │ ├── README.md │ └── gorush ├── core ├── core.go ├── health.go ├── queue.go └── storage.go ├── doc.go ├── docker-compose.yml ├── docker └── Dockerfile ├── go.mod ├── go.sum ├── helm └── gorush │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── configmap.yml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── k8s ├── gorush-aws-alb-ingress.yaml ├── gorush-configmap.yaml ├── gorush-deployment.yaml ├── gorush-namespace.yaml ├── gorush-redis-deployment.yaml ├── gorush-redis-service.yaml └── gorush-service.yaml ├── logx ├── log.go ├── log │ ├── .gitkeep │ └── access.log ├── log_interface.go └── log_test.go ├── main.go ├── metric ├── metrics.go └── metrics_test.go ├── netlify.toml ├── notify ├── feedback.go ├── feedback_test.go ├── global.go ├── main_test.go ├── notification.go ├── notification_apns.go ├── notification_apns_test.go ├── notification_fcm.go ├── notification_fcm_test.go ├── notification_hms.go ├── notification_hms_test.go └── notification_test.go ├── router ├── server.go ├── server_lambda.go ├── server_normal.go ├── server_test.go └── version.go ├── rpc ├── client_grpc_health.go ├── client_test.go ├── example │ ├── go │ │ ├── health │ │ │ └── main.go │ │ └── send │ │ │ └── main.go │ └── node │ │ ├── .gitignore │ │ ├── README.md │ │ ├── client.js │ │ ├── gorush_grpc_pb.js │ │ ├── gorush_pb.js │ │ ├── package-lock.json │ │ └── package.json ├── proto │ ├── gorush.pb.go │ ├── gorush.proto │ └── gorush_grpc.pb.go ├── server.go └── server_test.go ├── screenshot ├── lambda.png ├── memory.png ├── metrics.png └── status.png ├── status ├── status.go ├── status_test.go └── storage.go ├── storage ├── badger │ ├── badger.go │ └── badger_test.go ├── boltdb │ ├── boltdb.go │ └── boltdb_test.go ├── buntdb │ ├── buntdb.go │ └── buntdb_test.go ├── leveldb │ ├── leveldb.go │ └── leveldb_test.go ├── memory │ ├── memory.go │ └── memory_test.go ├── redis │ ├── redis.go │ └── redis_test.go └── storage.go └── tests ├── README.md └── test.json /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !release/ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: appleboy 4 | patreon: # Replace with a single Patreon username 5 | open_collective: gorush 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.paypal.me/appleboy46'] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: "30 1 * * 0" 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | permissions: 23 | # required for all workflows 24 | security-events: write 25 | 26 | # only required for workflows in private repositories 27 | actions: read 28 | contents: read 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | # Override automatic language detection by changing the below list 34 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 35 | # TODO: Enable for javascript later 36 | language: ["go"] 37 | 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v4 41 | 42 | # Initializes the CodeQL tools for scanning. 43 | - name: Initialize CodeQL 44 | uses: github/codeql-action/init@v3 45 | with: 46 | languages: ${{ matrix.language }} 47 | # If you wish to specify custom queries, you can do so here or in a config file. 48 | # By default, queries listed here will override any specified in a config file. 49 | # Prefix the list here with "+" to use these queries and those in the config file. 50 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 51 | 52 | - name: Perform CodeQL Analysis 53 | uses: github/codeql-action/analyze@v3 54 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - "master" 12 | 13 | jobs: 14 | build-docker: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version-file: go.mod 26 | check-latest: true 27 | 28 | - name: Build binary 29 | run: | 30 | make build_linux_amd64 31 | make build_linux_arm 32 | make build_linux_arm64 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: Login to Docker Hub 40 | uses: docker/login-action@v3 41 | with: 42 | username: ${{ secrets.DOCKERHUB_USERNAME }} 43 | password: ${{ secrets.DOCKERHUB_TOKEN }} 44 | 45 | - name: Login to GitHub Container Registry 46 | uses: docker/login-action@v3 47 | with: 48 | registry: ghcr.io 49 | username: ${{ github.repository_owner }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | - name: Docker meta 53 | id: docker-meta 54 | uses: docker/metadata-action@v5 55 | with: 56 | images: | 57 | ${{ github.repository }} 58 | ghcr.io/${{ github.repository }} 59 | tags: | 60 | type=raw,value=latest,enable={{is_default_branch}} 61 | type=semver,pattern={{version}} 62 | type=semver,pattern={{major}}.{{minor}} 63 | type=semver,pattern={{major}} 64 | 65 | - name: Build and push 66 | uses: docker/build-push-action@v5 67 | with: 68 | context: . 69 | platforms: linux/amd64,linux/arm,linux/arm64 70 | file: docker/Dockerfile 71 | push: ${{ github.event_name != 'pull_request' }} 72 | tags: ${{ steps.docker-meta.outputs.tags }} 73 | labels: ${{ steps.docker-meta.outputs.labels }} 74 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v6 27 | with: 28 | # either 'goreleaser' (default) or 'goreleaser-pro' 29 | distribution: goreleaser 30 | version: latest 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Run Lint and Testing 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version-file: go.mod 17 | check-latest: true 18 | 19 | - name: check golangci-lint 20 | uses: golangci/golangci-lint-action@v7 21 | with: 22 | version: v2.0 23 | args: --verbose 24 | 25 | - uses: hadolint/hadolint-action@v3.1.0 26 | name: hadolint for Dockerfile 27 | with: 28 | dockerfile: docker/Dockerfile 29 | 30 | testing: 31 | runs-on: ubuntu-latest 32 | container: node:16-bullseye 33 | 34 | # Service containers to run with `container-job` 35 | services: 36 | # Label used to access the service container 37 | redis: 38 | # Docker Hub image 39 | image: redis 40 | # Set health checks to wait until redis has started 41 | options: >- 42 | --health-cmd "redis-cli ping" 43 | --health-interval 10s 44 | --health-timeout 5s 45 | --health-retries 5 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v4 49 | 50 | - name: Setup go 51 | uses: actions/setup-go@v5 52 | with: 53 | go-version-file: go.mod 54 | check-latest: true 55 | 56 | - name: testing 57 | env: 58 | FCM_CREDENTIAL: ${{ secrets.FCM_CREDENTIAL }} 59 | FCM_TEST_TOKEN: ${{ secrets.FCM_TEST_TOKEN }} 60 | run: make test 61 | 62 | - name: Upload coverage to Codecov 63 | uses: codecov/codecov-action@v5 64 | with: 65 | flags: ${{ matrix.os }},go-${{ matrix.go }} 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | gin-bin 27 | key.pem 28 | .DS_Store 29 | gorush/log/*.log 30 | gorush.db 31 | .cover 32 | *.db* 33 | coverage.txt 34 | dist 35 | custom 36 | release 37 | coverage.txt 38 | node_modules 39 | config.yml 40 | dist/ 41 | .idea -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | - copyloopvar 7 | - dogsled 8 | - dupl 9 | - errcheck 10 | - exhaustive 11 | - gochecknoinits 12 | - goconst 13 | - gocritic 14 | - gocyclo 15 | - goprintffuncname 16 | - gosec 17 | - govet 18 | - ineffassign 19 | - lll 20 | - misspell 21 | - nakedret 22 | - noctx 23 | - nolintlint 24 | - staticcheck 25 | - unconvert 26 | - unparam 27 | - unused 28 | - whitespace 29 | exclusions: 30 | generated: lax 31 | presets: 32 | - comments 33 | - common-false-positives 34 | - legacy 35 | - std-error-handling 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | formatters: 41 | enable: 42 | - gofmt 43 | - gofumpt 44 | - goimports 45 | exclusions: 46 | generated: lax 47 | paths: 48 | - third_party$ 49 | - builtin$ 50 | - examples$ 51 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - darwin 12 | - linux 13 | - windows 14 | - freebsd 15 | goarch: 16 | - "386" 17 | - amd64 18 | - arm 19 | - arm64 20 | goarm: 21 | - "5" 22 | - "6" 23 | - "7" 24 | ignore: 25 | - goos: darwin 26 | goarch: arm 27 | - goos: darwin 28 | goarch: ppc64le 29 | - goos: darwin 30 | goarch: s390x 31 | - goos: windows 32 | goarch: ppc64le 33 | - goos: windows 34 | goarch: s390x 35 | - goos: windows 36 | goarch: arm 37 | goarm: "5" 38 | - goos: windows 39 | goarch: arm 40 | goarm: "6" 41 | - goos: windows 42 | goarch: arm 43 | goarm: "7" 44 | - goos: windows 45 | goarch: arm64 46 | - goos: freebsd 47 | goarch: ppc64le 48 | - goos: freebsd 49 | goarch: s390x 50 | - goos: freebsd 51 | goarch: arm 52 | goarm: "5" 53 | - goos: freebsd 54 | goarch: arm 55 | goarm: "6" 56 | - goos: freebsd 57 | goarch: arm 58 | goarm: "7" 59 | - goos: freebsd 60 | goarch: arm64 61 | flags: 62 | - -trimpath 63 | ldflags: 64 | - -s -w 65 | - -X main.version={{.Version}} 66 | - -X main.commit={{.ShortCommit}} 67 | binary: >- 68 | {{ .ProjectName }}- 69 | {{- if .IsSnapshot }}{{ .Branch }}- 70 | {{- else }}{{- .Version }}-{{ end }} 71 | {{- .Os }}- 72 | {{- if eq .Arch "amd64" }}amd64 73 | {{- else if eq .Arch "amd64_v1" }}amd64 74 | {{- else if eq .Arch "386" }}386 75 | {{- else }}{{ .Arch }}{{ end }} 76 | {{- if .Arm }}-{{ .Arm }}{{ end }} 77 | no_unique_dist_dir: true 78 | 79 | archives: 80 | - format: binary 81 | name_template: "{{ .Binary }}" 82 | allow_different_binary_count: true 83 | 84 | checksum: 85 | name_template: "checksums.txt" 86 | 87 | snapshot: 88 | version_template: "{{ incpatch .Version }}" 89 | 90 | changelog: 91 | use: github 92 | groups: 93 | - title: Features 94 | regexp: "^.*feat[(\\w)]*:+.*$" 95 | order: 0 96 | - title: "Bug fixes" 97 | regexp: "^.*fix[(\\w)]*:+.*$" 98 | order: 1 99 | - title: "Enhancements" 100 | regexp: "^.*chore[(\\w)]*:+.*$" 101 | order: 2 102 | - title: "Refactor" 103 | regexp: "^.*refactor[(\\w)]*:+.*$" 104 | order: 3 105 | - title: "Build process updates" 106 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 107 | order: 4 108 | - title: "Documentation updates" 109 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 110 | order: 4 111 | - title: Others 112 | order: 999 113 | -------------------------------------------------------------------------------- /.hadolint.yaml: -------------------------------------------------------------------------------- 1 | ignored: 2 | - DL3018 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bo-Yi Wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXECUTABLE := gorush 2 | GO ?= go 3 | GOFMT ?= gofumpt -l -s -extra 4 | GOFILES := $(shell find . -name "*.go" -type f) 5 | TAGS ?= sqlite 6 | LDFLAGS ?= -X main.version=$(VERSION) -X main.commit=$(COMMIT) 7 | 8 | PROTOC_GEN_GO=v1.36.6 9 | PROTOC_GEN_GO_GRPC=v1.5.1 10 | 11 | ifneq ($(shell uname), Darwin) 12 | EXTLDFLAGS = -extldflags "-static" $(null) 13 | else 14 | EXTLDFLAGS = 15 | endif 16 | 17 | ifneq ($(DRONE_TAG),) 18 | VERSION ?= $(DRONE_TAG) 19 | else 20 | VERSION ?= $(shell git describe --tags --always || git rev-parse --short HEAD) 21 | endif 22 | 23 | COMMIT ?= $(shell git rev-parse --short HEAD) 24 | 25 | all: build 26 | 27 | .PHONY: help 28 | help: ## Print this help message. 29 | @echo "Usage: make [target]" 30 | @echo "" 31 | @echo "Targets:" 32 | @echo "" 33 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 34 | 35 | init: ## check the FCM_CREDENTIAL and FCM_TEST_TOKEN 36 | @echo "==> Check FCM_CREDENTIAL and FCM_TEST_TOKEN" 37 | ifeq ($(FCM_CREDENTIAL),) 38 | @echo "Missing FCM_CREDENTIAL Parameter" 39 | @exit 1 40 | endif 41 | ifeq ($(FCM_TEST_TOKEN),) 42 | @echo "Missing FCM_TEST_TOKEN Parameter" 43 | @exit 1 44 | endif 45 | @echo "Already set FCM_CREDENTIAL and endif global variable." 46 | 47 | .PHONY: install ## Install the gorush binary 48 | install: $(GOFILES) 49 | $(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' 50 | @echo "\n==>\033[32m Installed gorush to ${GOPATH}/bin/gorush\033[m" 51 | 52 | .PHONY: build ## Build the gorush binary 53 | build: $(EXECUTABLE) 54 | 55 | .PHONY: $(EXECUTABLE) 56 | $(EXECUTABLE): $(GOFILES) 57 | $(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/$@ 58 | 59 | .PHONY: test ## Run the tests 60 | test: init 61 | @$(GO) test -v -cover -tags $(TAGS) -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1 62 | 63 | build_linux_amd64: ## build the gorush binary for linux amd64 64 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/amd64/$(EXECUTABLE) 65 | 66 | build_linux_i386: ## build the gorush binary for linux i386 67 | CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/i386/$(EXECUTABLE) 68 | 69 | build_linux_arm64: ## build the gorush binary for linux arm64 70 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm64/$(EXECUTABLE) 71 | 72 | build_linux_arm: ## build the gorush binary for linux arm 73 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm/$(EXECUTABLE) 74 | 75 | build_linux_lambda: ## build the gorush binary for linux lambda 76 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags 'lambda' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/lambda/$(EXECUTABLE) 77 | 78 | build_darwin_amd64: ## build the gorush binary for darwin amd64 79 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/amd64/$(EXECUTABLE) 80 | 81 | build_darwin_i386: ## build the gorush binary for darwin i386 82 | CGO_ENABLED=0 GOOS=darwin GOARCH=386 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/i386/$(EXECUTABLE) 83 | 84 | build_darwin_arm64: ## build the gorush binary for darwin arm64 85 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/arm64/$(EXECUTABLE) 86 | 87 | build_darwin_arm: ## build the gorush binary for darwin arm 88 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm GOARM=7 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/arm/$(EXECUTABLE) 89 | 90 | build_darwin_lambda: ## build the gorush binary for darwin lambda 91 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -tags 'lambda' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/darwin/lambda/$(EXECUTABLE) 92 | 93 | clean: ## Clean the build 94 | $(GO) clean -modcache -x -i ./... 95 | find . -name coverage.txt -delete 96 | find . -name *.tar.gz -delete 97 | find . -name *.db -delete 98 | -rm -rf release dist .cover 99 | 100 | .PHONY: proto_install 101 | proto_install: ## install the protoc-gen-go and protoc-gen-go-grpc 102 | $(GO) install google.golang.org/protobuf/cmd/protoc-gen-go@$(PROTOC_GEN_GO) 103 | $(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$(PROTOC_GEN_GO_GRPC) 104 | 105 | generate_proto_js: ## generate the proto file for nodejs 106 | npm install grpc-tools 107 | protoc -I rpc/proto rpc/proto/gorush.proto --js_out=import_style=commonjs,binary:rpc/example/node/ --grpc_out=rpc/example/node/ --plugin=protoc-gen-grpc="node_modules/.bin/grpc_tools_node_protoc_plugin" 108 | 109 | generate_proto_go: ## generate the proto file for golang 110 | protoc -I rpc/proto rpc/proto/gorush.proto --go_out=rpc/proto --go-grpc_out=require_unimplemented_servers=false:rpc/proto 111 | 112 | generate_proto: generate_proto_go generate_proto_js 113 | 114 | .PHONY: air 115 | air: ## install air for hot reload 116 | @hash air > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 117 | $(GO) install github.com/cosmtrek/air@latest; \ 118 | fi 119 | 120 | .PHONY: dev ## run the air for hot reload 121 | dev: air 122 | air --build.cmd "make" --build.bin release/gorush 123 | 124 | version: ## print the version 125 | @echo $(VERSION) 126 | -------------------------------------------------------------------------------- /certificate/authkey-invalid.p8: -------------------------------------------------------------------------------- 1 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE 2 | ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI 3 | tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfZZZ 4 | -------------------------------------------------------------------------------- /certificate/authkey-valid.p8: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE 3 | ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI 4 | tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfeQl 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /certificate/certificate-valid.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gorush/316fa2605d16e50a32415560ab81d81b5a9bd1f9/certificate/certificate-valid.p12 -------------------------------------------------------------------------------- /certificate/certificate-valid.pem: -------------------------------------------------------------------------------- 1 | Bag Attributes 2 | localKeyID: 8C 1A 9F 00 66 BD 24 42 B9 5D 1E EB FE 5E 8B CA 04 3D 73 83 3 | friendlyName: APNS/2 Private Key 4 | subject=/C=NZ/ST=Wellington/L=Wellington/O=Internet Widgits Pty Ltd/OU=9ZEH62KRVV/CN=APNS/2 Development IOS Push Services: com.sideshow.Apns2 5 | issuer=/C=NZ/ST=Wellington/L=Wellington/O=APNS/2 Inc./OU=APNS/2 Worldwide Developer Relations/CN=APNS/2 Worldwide Developer Relations Certification Authority 6 | -----BEGIN CERTIFICATE----- 7 | MIID6zCCAtMCAQIwDQYJKoZIhvcNAQELBQAwgcMxCzAJBgNVBAYTAk5aMRMwEQYD 8 | VQQIEwpXZWxsaW5ndG9uMRMwEQYDVQQHEwpXZWxsaW5ndG9uMRQwEgYDVQQKEwtB 9 | UE5TLzIgSW5jLjEtMCsGA1UECxMkQVBOUy8yIFdvcmxkd2lkZSBEZXZlbG9wZXIg 10 | UmVsYXRpb25zMUUwQwYDVQQDEzxBUE5TLzIgV29ybGR3aWRlIERldmVsb3BlciBS 11 | ZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTYwMTA4MDgzNDMw 12 | WhcNMjYwMTA1MDgzNDMwWjCBsjELMAkGA1UEBhMCTloxEzARBgNVBAgTCldlbGxp 13 | bmd0b24xEzARBgNVBAcTCldlbGxpbmd0b24xITAfBgNVBAoTGEludGVybmV0IFdp 14 | ZGdpdHMgUHR5IEx0ZDETMBEGA1UECxMKOVpFSDYyS1JWVjFBMD8GA1UEAxM4QVBO 15 | Uy8yIERldmVsb3BtZW50IElPUyBQdXNoIFNlcnZpY2VzOiBjb20uc2lkZXNob3cu 16 | QXBuczIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY0c1TKB5oZPwQ 17 | 7t1CwMIrvqB6GIU3tPy6RhckZXTkOB8YeBWJ7UKfCz8HGHFVomBP0T5OUbeqQzqW 18 | YJbQzZ8a6ZMszbL0lO4X9++3Oi5/TtAwOUOK8rOFN25m2KfsayHQZ/4vWStK2Fwm 19 | 5aJbGLlpH/b/7z1D4vhmMgoBuT1IuyhGiyFxlZ9EtTloFvsqM1E5fYZOSZACyXTa 20 | K4vdgbQMgUVsI714FAgLTlK0UeiRkmKm3pdbtfVbrthzI+IHXKItUIy+Fn20PRMh 21 | dSnaztSz7tgBWCIx22qvcYogHWiOgUYIM772zE2y8UVOr8DsiRlsOHSA7EI4MJcQ 22 | G2FUq2Z/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGyfyO2HMgcdeBcz3bt5BILX 23 | f7RA2/UmVIwcKR1qotTsF+PnBmcILeyOQgDe9tGU5cRc79kDt3JRmMYROFIMgFRf 24 | Wf22uOKtho7GQQaKvG+bkgMVdYFRlBHnF+KeqKH81qb9p+CT4Iw0GehIL1DijFLR 25 | VIAIBYpz4oBPCIE1ISVT+Fgaf3JAh59kbPbNw9AIDxaBtP8EuzSTNwfbxoGbCobS 26 | Wi1U8IsCwQFt8tM1m4ZXD1CcZIrGdryeAhVkvKIJRiU5QYWI2nqZN+JqQucm9ad0 27 | mYO5mJkIobUa4+ZJhCPKEdmgpFbRGk0wVuaDM9Cv6P2srsYAjaO4y3VP0GvNKRI= 28 | -----END CERTIFICATE----- 29 | Bag Attributes 30 | localKeyID: 8C 1A 9F 00 66 BD 24 42 B9 5D 1E EB FE 5E 8B CA 04 3D 73 83 31 | friendlyName: APNS/2 Private Key 32 | Key Attributes: 33 | -----BEGIN RSA PRIVATE KEY----- 34 | MIIEowIBAAKCAQEA2NHNUygeaGT8EO7dQsDCK76gehiFN7T8ukYXJGV05DgfGHgV 35 | ie1Cnws/BxhxVaJgT9E+TlG3qkM6lmCW0M2fGumTLM2y9JTuF/fvtzouf07QMDlD 36 | ivKzhTduZtin7Gsh0Gf+L1krSthcJuWiWxi5aR/2/+89Q+L4ZjIKAbk9SLsoRosh 37 | cZWfRLU5aBb7KjNROX2GTkmQAsl02iuL3YG0DIFFbCO9eBQIC05StFHokZJipt6X 38 | W7X1W67YcyPiB1yiLVCMvhZ9tD0TIXUp2s7Us+7YAVgiMdtqr3GKIB1ojoFGCDO+ 39 | 9sxNsvFFTq/A7IkZbDh0gOxCODCXEBthVKtmfwIDAQABAoIBAQCW8ZCI+OAae1tE 40 | ipZ9F2bWP3LHLXTo8FYVdCA+VWeITk3PoiIUkJmV0aWCUhDstgto5doDej5sCTur 41 | Xvj/ynaerMeqJFYWkewjwZcgLyAZvwuO1v7fp9E0x/9TGDfnjjnPNeaundxW0cNt 42 | zOY3l0HVHsy9Jpe3QDcAJovy4Tv5+hFY4kDxUBGsyjvhScVgKg5tLkJclm3sOu/L 43 | GyLqpwNI3OJAdMIuVD4N2BZ1aOEap6mp2y8Ie0/R4YWcaZ5A4Pw7xUPl6SXc9uua 44 | /78QTERtPC6ejyCBiE05a8m3Q3iud3Xtnlyws2KwhgBAfE6M4zR/f3OQB7ZIXMhy 45 | ZpmZZw5xAoGBAPYn84IrlIQetWQfvPdM7Kzgh6UDHCugnlCDghwYpRJGi8hMfuZV 46 | xNIrYAJzLYDQ01lFJRJgWXTcbqz9NBz1nhg+cNOz1/KY+38eudee6DNYmztP7jDP 47 | 2jnaS+dtjC8hAXObnFqG+NilMDLLu6aRmrJaImbjSrfyLiE6mvJ7u81nAoGBAOF9 48 | g93wZ0mL1rk2s5WwHGTNU/HaOtmWS4z7kA7f4QaRub+MwppZmmDZPHpiZX7BPcZz 49 | iOPQh+xn7IqRGoQWBLykBVt8zZFoLZJoCR3n63lex5A4p/0Pp1gFZrR+xX8PYVos 50 | 3yeeiWyPKsXXNc0s5QwHZcX6Wb8EHThTXGCBetcpAoGAMeQJC9IPaPPcae2w3CLA 51 | OY3MkFpgBEuqqsDsxwsLsfeQb0lp0v+BQ+O8suJrT5eDrq1ABUh3+SKQYAl13YS+ 52 | xUUqkw35b9cn6iztF9HCWF3WIKBjs4r9PQqMpdxjNE4pQChC+Wov16ErcrAuWWVb 53 | iFiSbm4U/9FbHisFqq3/c3MCgYB+vzSuPgFw37+0oEDVtQZgyuGSop5NzCNvfb/9 54 | /G3aaXNFbnO8mv0hzzoleMWgODLnJ+4cUAz3H3tgcCu9bzr+Zhv0zvQl9a8YCo6F 55 | VuWPdW0rbg1PO8tOuMqATnno79ZC/9H3zS9l7BuY1V2SlNeyqT3VyOFFc6SREpps 56 | TJul8QKBgAxnQB8MA7zPULu1clyaJLdtEdRPkKWN7lKYptc0e/VHfSsKxseWkfqi 57 | zgXZ51kQTrT6Zb6HYRfwC1mMXHWRKRyYjAnCxVim6YQd+KVT49iRDDAiIFoMGA4i 58 | vvcIlneqOZZPDIoKJ60IjO/DZHWkw5mLjaIrT+qQ3XAGdJA13hcm 59 | -----END RSA PRIVATE KEY----- 60 | -------------------------------------------------------------------------------- /certificate/localhost.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+zCCAeOgAwIBAgIJALbZEDvUQrFKMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xNjAzMjgwMzMwNDFaFw0yNjAzMjYwMzMwNDFaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAMj1+xg4jVLzVnB5j7n1ul30WEE4BCzcNFxg5AOB5H5q+wje0YYiVFg6PQyv 6 | GCipqIRXVRdVQ1hHSeunYGKe8lq3Sb1X8PUJ12v9uRbpS9DK1Owqk8rsPDu6sVTL 7 | qKKgH1Z8yazzaS0AbXuA5e9gO/RzijbnpEP+quM4dueiMPVEJyLq+EoIQY+MM8MP 8 | 8dZzL4XZl7wL4UsCN7rPcO6W3tlnT0iO3h9c/Ym2hFhz+KNJ9KRRCvtPGZESigtK 9 | bHsXH099WDo8v/Wp5/evBw/+JD0opxmCfHIBALHt9v53RvvsDZ1t33Rpu5C8znEY 10 | Y2Ay7NgxhqjqoWJqA48lJeA0clsCAwEAAaNQME4wHQYDVR0OBBYEFC0bTU1Xofeh 11 | NKIelashIsqKidDYMB8GA1UdIwQYMBaAFC0bTU1XofehNKIelashIsqKidDYMAwG 12 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAiJL8IMTwNX9XqQWYDFgkG4 13 | AnrVwQhreAqC9rSxDCjqqnMHPHGzcCeDMLAMoh0kOy20nowUGNtCZ0uBvnX2q1bN 14 | g1jt+GBcLJDR3LL4CpNOlm3YhOycuNfWMxTA7BXkmnSrZD/7KhArsBEY8aulxwKJ 15 | HRgNlIwe1oFD1YdX1BS5pp4t25B6Vq4A3FMMUkVoWE688nE168hvQgwjrHkgHhwe 16 | eN8lGE2DhFraXnWmDMdwaHD3HRFGhyppIFN+f7BqbWX9gM+T2YRTfObIXLWbqJLD 17 | 3Mk/NkxqVcg4eY54wJ1ufCUGAYAIaY6fQqiNUz8nhwK3t45NBVT9y/uJXqnTLyY= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /certificate/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAyPX7GDiNUvNWcHmPufW6XfRYQTgELNw0XGDkA4Hkfmr7CN7R 3 | hiJUWDo9DK8YKKmohFdVF1VDWEdJ66dgYp7yWrdJvVfw9QnXa/25FulL0MrU7CqT 4 | yuw8O7qxVMuooqAfVnzJrPNpLQBte4Dl72A79HOKNuekQ/6q4zh256Iw9UQnIur4 5 | SghBj4wzww/x1nMvhdmXvAvhSwI3us9w7pbe2WdPSI7eH1z9ibaEWHP4o0n0pFEK 6 | +08ZkRKKC0psexcfT31YOjy/9ann968HD/4kPSinGYJ8cgEAse32/ndG++wNnW3f 7 | dGm7kLzOcRhjYDLs2DGGqOqhYmoDjyUl4DRyWwIDAQABAoIBAGTKqsN9KbSfA42q 8 | CqI0UuLouJMNa1qsnz5uAi6YKWgWdA4A44mpEjCmFRSVhUJvxWuK+cyYIQzXxIWD 9 | D16nZdqF72AeCWZ9JySsvvZ00GfKM3y35iRy08sJWgOzmcLnGJCiSeyKsQe3HTJC 10 | dhDXbXqvsHTVPZg01LTeDxUiTffU8NMKqR2AecQ2sTDwXEhAnTyAtnzl/XaBgFzu 11 | U6G7FzGM5y9bxkfQVkvy+DEJkHGNOjzwcVfByyVl610ixmG1vmxVj9PbWmIPsUV8 12 | ySmjhvDQbOfoxW0h9vTlTqGtQcBw962osnDDMWFCdM7lzO0T7RRnPVGIRpCJOKhq 13 | keqHKwECgYEA8wwI/iZughoTXTNG9LnQQ/WAtsqO80EjMTUheo5I1kOzmUz09pyh 14 | iAsUDoN0/26tZ5WNjlnyZu7dvTc/x3dTZpmNnoo8gcVbQNECDRzqfuQ9PPXm1SN5 15 | 6peBqAvBv78hjV05aXzPG/VBbeig7l299EarEA+a/oH3KrgDoqVqE0ECgYEA06vA 16 | YJmgg4fZRucAYoaYsLz9Z9rCFjTe1PBTmUJkbOR8vFIHHTTEWi/SuxXL0wDSeoE2 17 | 7BQm86gCC7/KgRdrzoBqZ5qS9Mv2dsLgY635VSgjjfZkVLiH1VRRpSQObYnfoysg 18 | gatcHSKMExd4SLQByAuImXP+L5ayDBcEJfbqSpsCgYB78Is1b0uzNLDjOh7Y9Vhr 19 | D2qPzEORcIoNsdZctOoXuXaAmmngyIbm5R9ZN1gWWc47oFwLV3rxWqXgs6fmg8cX 20 | 7v309vFcC9Q4/Vxaa4B5LNK9n3gTAIBPTOtlUnl+2my1tfBtBqRm0W6IKbTHWS5g 21 | vxjEm/CiEIyGUEgqTMgHAQKBgBKuXdQoutng63QufwIzDtbKVzMLQ4XiNKhmbXph 22 | OavCnp+gPbB+L7Yl8ltAmTSOJgVZ0hcT0DxA361Zx+2Mu58GBl4OblnchmwE1vj1 23 | KcQyPrEQxdoUTyiswGfqvrs8J9imvb+z9/U6T1KAB8Wi3WViXzPr4MsiaaRXg642 24 | FIdxAoGAZ7/735dkhJcyOfs+LKsLr68JSstoorXOYvdMu1+JGa9iLuhnHEcMVWC8 25 | IuihzPfloZtMbGYkZJn8l3BeGd8hmfFtgTgZGPoVRetft2LDFLnPxp2sEH5OFLsQ 26 | R+K/kAOul8eStWuMXOFA9pMzGkGEgIFJMJOyaJON3kedQI8deCM= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | // Test file is missing 13 | func TestMissingFile(t *testing.T) { 14 | filename := "test" 15 | _, err := LoadConf(filename) 16 | 17 | assert.NotNil(t, err) 18 | } 19 | 20 | func TestEmptyConfig(t *testing.T) { 21 | conf, err := LoadConf("testdata/empty.yml") 22 | if err != nil { 23 | panic("failed to load config.yml from file") 24 | } 25 | 26 | assert.Equal(t, uint(100), conf.Ios.MaxConcurrentPushes) 27 | } 28 | 29 | type ConfigTestSuite struct { 30 | suite.Suite 31 | ConfGorushDefault *ConfYaml 32 | ConfGorush *ConfYaml 33 | } 34 | 35 | func (suite *ConfigTestSuite) SetupTest() { 36 | var err error 37 | suite.ConfGorushDefault, err = LoadConf() 38 | if err != nil { 39 | panic("failed to load default config.yml") 40 | } 41 | suite.ConfGorush, err = LoadConf("testdata/config.yml") 42 | if err != nil { 43 | panic("failed to load config.yml from file") 44 | } 45 | } 46 | 47 | func (suite *ConfigTestSuite) TestValidateConfDefault() { 48 | // Core 49 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.Address) 50 | assert.Equal(suite.T(), "8088", suite.ConfGorushDefault.Core.Port) 51 | assert.Equal(suite.T(), int64(30), suite.ConfGorushDefault.Core.ShutdownTimeout) 52 | assert.Equal(suite.T(), true, suite.ConfGorushDefault.Core.Enabled) 53 | assert.Equal(suite.T(), int64(runtime.NumCPU()), suite.ConfGorushDefault.Core.WorkerNum) 54 | assert.Equal(suite.T(), int64(8192), suite.ConfGorushDefault.Core.QueueNum) 55 | assert.Equal(suite.T(), "release", suite.ConfGorushDefault.Core.Mode) 56 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.Sync) 57 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.FeedbackURL) 58 | assert.Equal(suite.T(), 0, len(suite.ConfGorushDefault.Core.FeedbackHeader)) 59 | assert.Equal(suite.T(), int64(10), suite.ConfGorushDefault.Core.FeedbackTimeout) 60 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.SSL) 61 | assert.Equal(suite.T(), "cert.pem", suite.ConfGorushDefault.Core.CertPath) 62 | assert.Equal(suite.T(), "key.pem", suite.ConfGorushDefault.Core.KeyPath) 63 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.KeyBase64) 64 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.CertBase64) 65 | assert.Equal(suite.T(), int64(100), suite.ConfGorushDefault.Core.MaxNotification) 66 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.HTTPProxy) 67 | // Pid 68 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.PID.Enabled) 69 | assert.Equal(suite.T(), "gorush.pid", suite.ConfGorushDefault.Core.PID.Path) 70 | assert.Equal(suite.T(), true, suite.ConfGorushDefault.Core.PID.Override) 71 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.AutoTLS.Enabled) 72 | assert.Equal(suite.T(), ".cache", suite.ConfGorushDefault.Core.AutoTLS.Folder) 73 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.AutoTLS.Host) 74 | 75 | // Api 76 | assert.Equal(suite.T(), "/api/push", suite.ConfGorushDefault.API.PushURI) 77 | assert.Equal(suite.T(), "/api/stat/go", suite.ConfGorushDefault.API.StatGoURI) 78 | assert.Equal(suite.T(), "/api/stat/app", suite.ConfGorushDefault.API.StatAppURI) 79 | assert.Equal(suite.T(), "/api/config", suite.ConfGorushDefault.API.ConfigURI) 80 | assert.Equal(suite.T(), "/sys/stats", suite.ConfGorushDefault.API.SysStatURI) 81 | assert.Equal(suite.T(), "/metrics", suite.ConfGorushDefault.API.MetricURI) 82 | assert.Equal(suite.T(), "/healthz", suite.ConfGorushDefault.API.HealthURI) 83 | 84 | // Android 85 | assert.Equal(suite.T(), true, suite.ConfGorushDefault.Android.Enabled) 86 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Android.KeyPath) 87 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Android.Credential) 88 | assert.Equal(suite.T(), 0, suite.ConfGorushDefault.Android.MaxRetry) 89 | 90 | // iOS 91 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Ios.Enabled) 92 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.KeyPath) 93 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.KeyBase64) 94 | assert.Equal(suite.T(), "pem", suite.ConfGorushDefault.Ios.KeyType) 95 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.Password) 96 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Ios.Production) 97 | assert.Equal(suite.T(), uint(100), suite.ConfGorushDefault.Ios.MaxConcurrentPushes) 98 | assert.Equal(suite.T(), 0, suite.ConfGorushDefault.Ios.MaxRetry) 99 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.KeyID) 100 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.TeamID) 101 | 102 | // queue 103 | assert.Equal(suite.T(), "local", suite.ConfGorushDefault.Queue.Engine) 104 | assert.Equal(suite.T(), "127.0.0.1:4150", suite.ConfGorushDefault.Queue.NSQ.Addr) 105 | assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.NSQ.Topic) 106 | assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.NSQ.Channel) 107 | 108 | assert.Equal(suite.T(), "127.0.0.1:4222", suite.ConfGorushDefault.Queue.NATS.Addr) 109 | assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.NATS.Subj) 110 | assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.NATS.Queue) 111 | 112 | assert.Equal(suite.T(), "127.0.0.1:6379", suite.ConfGorushDefault.Queue.Redis.Addr) 113 | assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.Redis.StreamName) 114 | assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.Redis.Group) 115 | assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.Redis.Consumer) 116 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Queue.Redis.Username) 117 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Queue.Redis.Password) 118 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Queue.Redis.WithTLS) 119 | 120 | // log 121 | assert.Equal(suite.T(), "string", suite.ConfGorushDefault.Log.Format) 122 | assert.Equal(suite.T(), "stdout", suite.ConfGorushDefault.Log.AccessLog) 123 | assert.Equal(suite.T(), "debug", suite.ConfGorushDefault.Log.AccessLevel) 124 | assert.Equal(suite.T(), "stderr", suite.ConfGorushDefault.Log.ErrorLog) 125 | assert.Equal(suite.T(), "error", suite.ConfGorushDefault.Log.ErrorLevel) 126 | assert.Equal(suite.T(), true, suite.ConfGorushDefault.Log.HideToken) 127 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Log.HideMessages) 128 | 129 | assert.Equal(suite.T(), "memory", suite.ConfGorushDefault.Stat.Engine) 130 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.Stat.Redis.Cluster) 131 | assert.Equal(suite.T(), "localhost:6379", suite.ConfGorushDefault.Stat.Redis.Addr) 132 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Stat.Redis.Username) 133 | assert.Equal(suite.T(), "", suite.ConfGorushDefault.Stat.Redis.Password) 134 | assert.Equal(suite.T(), 0, suite.ConfGorushDefault.Stat.Redis.DB) 135 | 136 | assert.Equal(suite.T(), "bolt.db", suite.ConfGorushDefault.Stat.BoltDB.Path) 137 | assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Stat.BoltDB.Bucket) 138 | 139 | assert.Equal(suite.T(), "bunt.db", suite.ConfGorushDefault.Stat.BuntDB.Path) 140 | assert.Equal(suite.T(), "level.db", suite.ConfGorushDefault.Stat.LevelDB.Path) 141 | assert.Equal(suite.T(), "badger.db", suite.ConfGorushDefault.Stat.BadgerDB.Path) 142 | 143 | // gRPC 144 | assert.Equal(suite.T(), false, suite.ConfGorushDefault.GRPC.Enabled) 145 | assert.Equal(suite.T(), "9000", suite.ConfGorushDefault.GRPC.Port) 146 | } 147 | 148 | func (suite *ConfigTestSuite) TestValidateConf() { 149 | // Core 150 | assert.Equal(suite.T(), "8088", suite.ConfGorush.Core.Port) 151 | assert.Equal(suite.T(), int64(30), suite.ConfGorush.Core.ShutdownTimeout) 152 | assert.Equal(suite.T(), true, suite.ConfGorush.Core.Enabled) 153 | assert.Equal(suite.T(), int64(runtime.NumCPU()), suite.ConfGorush.Core.WorkerNum) 154 | assert.Equal(suite.T(), int64(8192), suite.ConfGorush.Core.QueueNum) 155 | assert.Equal(suite.T(), "release", suite.ConfGorush.Core.Mode) 156 | assert.Equal(suite.T(), false, suite.ConfGorush.Core.Sync) 157 | assert.Equal(suite.T(), "", suite.ConfGorush.Core.FeedbackURL) 158 | assert.Equal(suite.T(), int64(10), suite.ConfGorush.Core.FeedbackTimeout) 159 | assert.Equal(suite.T(), 1, len(suite.ConfGorush.Core.FeedbackHeader)) 160 | assert.Equal(suite.T(), "x-gorush-token:4e989115e09680f44a645519fed6a976", suite.ConfGorush.Core.FeedbackHeader[0]) 161 | assert.Equal(suite.T(), false, suite.ConfGorush.Core.SSL) 162 | assert.Equal(suite.T(), "cert.pem", suite.ConfGorush.Core.CertPath) 163 | assert.Equal(suite.T(), "key.pem", suite.ConfGorush.Core.KeyPath) 164 | assert.Equal(suite.T(), "", suite.ConfGorush.Core.CertBase64) 165 | assert.Equal(suite.T(), "", suite.ConfGorush.Core.KeyBase64) 166 | assert.Equal(suite.T(), int64(100), suite.ConfGorush.Core.MaxNotification) 167 | assert.Equal(suite.T(), "", suite.ConfGorush.Core.HTTPProxy) 168 | // Pid 169 | assert.Equal(suite.T(), false, suite.ConfGorush.Core.PID.Enabled) 170 | assert.Equal(suite.T(), "gorush.pid", suite.ConfGorush.Core.PID.Path) 171 | assert.Equal(suite.T(), true, suite.ConfGorush.Core.PID.Override) 172 | assert.Equal(suite.T(), false, suite.ConfGorush.Core.AutoTLS.Enabled) 173 | assert.Equal(suite.T(), ".cache", suite.ConfGorush.Core.AutoTLS.Folder) 174 | assert.Equal(suite.T(), "", suite.ConfGorush.Core.AutoTLS.Host) 175 | 176 | // Api 177 | assert.Equal(suite.T(), "/api/push", suite.ConfGorush.API.PushURI) 178 | assert.Equal(suite.T(), "/api/stat/go", suite.ConfGorush.API.StatGoURI) 179 | assert.Equal(suite.T(), "/api/stat/app", suite.ConfGorush.API.StatAppURI) 180 | assert.Equal(suite.T(), "/api/config", suite.ConfGorush.API.ConfigURI) 181 | assert.Equal(suite.T(), "/sys/stats", suite.ConfGorush.API.SysStatURI) 182 | assert.Equal(suite.T(), "/metrics", suite.ConfGorush.API.MetricURI) 183 | assert.Equal(suite.T(), "/healthz", suite.ConfGorush.API.HealthURI) 184 | 185 | // Android 186 | assert.Equal(suite.T(), true, suite.ConfGorush.Android.Enabled) 187 | assert.Equal(suite.T(), "key.json", suite.ConfGorush.Android.KeyPath) 188 | assert.Equal(suite.T(), "CREDENTIAL_JSON_DATA", suite.ConfGorush.Android.Credential) 189 | assert.Equal(suite.T(), 0, suite.ConfGorush.Android.MaxRetry) 190 | 191 | // iOS 192 | assert.Equal(suite.T(), false, suite.ConfGorush.Ios.Enabled) 193 | assert.Equal(suite.T(), "key.pem", suite.ConfGorush.Ios.KeyPath) 194 | assert.Equal(suite.T(), "", suite.ConfGorush.Ios.KeyBase64) 195 | assert.Equal(suite.T(), "pem", suite.ConfGorush.Ios.KeyType) 196 | assert.Equal(suite.T(), "", suite.ConfGorush.Ios.Password) 197 | assert.Equal(suite.T(), false, suite.ConfGorush.Ios.Production) 198 | assert.Equal(suite.T(), uint(100), suite.ConfGorush.Ios.MaxConcurrentPushes) 199 | assert.Equal(suite.T(), 0, suite.ConfGorush.Ios.MaxRetry) 200 | assert.Equal(suite.T(), "", suite.ConfGorush.Ios.KeyID) 201 | assert.Equal(suite.T(), "", suite.ConfGorush.Ios.TeamID) 202 | 203 | // log 204 | assert.Equal(suite.T(), "string", suite.ConfGorush.Log.Format) 205 | assert.Equal(suite.T(), "stdout", suite.ConfGorush.Log.AccessLog) 206 | assert.Equal(suite.T(), "debug", suite.ConfGorush.Log.AccessLevel) 207 | assert.Equal(suite.T(), "stderr", suite.ConfGorush.Log.ErrorLog) 208 | assert.Equal(suite.T(), "error", suite.ConfGorush.Log.ErrorLevel) 209 | assert.Equal(suite.T(), true, suite.ConfGorush.Log.HideToken) 210 | 211 | assert.Equal(suite.T(), "memory", suite.ConfGorush.Stat.Engine) 212 | assert.Equal(suite.T(), false, suite.ConfGorush.Stat.Redis.Cluster) 213 | assert.Equal(suite.T(), "localhost:6379", suite.ConfGorush.Stat.Redis.Addr) 214 | assert.Equal(suite.T(), "", suite.ConfGorush.Stat.Redis.Username) 215 | assert.Equal(suite.T(), "", suite.ConfGorush.Stat.Redis.Password) 216 | assert.Equal(suite.T(), 0, suite.ConfGorush.Stat.Redis.DB) 217 | 218 | assert.Equal(suite.T(), "bolt.db", suite.ConfGorush.Stat.BoltDB.Path) 219 | assert.Equal(suite.T(), "gorush", suite.ConfGorush.Stat.BoltDB.Bucket) 220 | 221 | assert.Equal(suite.T(), "bunt.db", suite.ConfGorush.Stat.BuntDB.Path) 222 | assert.Equal(suite.T(), "level.db", suite.ConfGorush.Stat.LevelDB.Path) 223 | assert.Equal(suite.T(), "badger.db", suite.ConfGorush.Stat.BadgerDB.Path) 224 | 225 | // gRPC 226 | assert.Equal(suite.T(), false, suite.ConfGorush.GRPC.Enabled) 227 | assert.Equal(suite.T(), "9000", suite.ConfGorush.GRPC.Port) 228 | } 229 | 230 | func TestConfigTestSuite(t *testing.T) { 231 | suite.Run(t, new(ConfigTestSuite)) 232 | } 233 | 234 | func TestLoadConfigFromEnv(t *testing.T) { 235 | os.Setenv("GORUSH_CORE_PORT", "9001") 236 | os.Setenv("GORUSH_GRPC_ENABLED", "true") 237 | os.Setenv("GORUSH_CORE_MAX_NOTIFICATION", "200") 238 | os.Setenv("GORUSH_IOS_KEY_ID", "ABC123DEFG") 239 | os.Setenv("GORUSH_IOS_TEAM_ID", "DEF123GHIJ") 240 | os.Setenv("GORUSH_API_HEALTH_URI", "/healthz") 241 | os.Setenv("GORUSH_CORE_FEEDBACK_HOOK_URL", "http://example.com") 242 | os.Setenv("GORUSH_CORE_FEEDBACK_HEADER", "x-api-key:1234567890 x-auth-key:0987654321") 243 | ConfGorush, err := LoadConf("testdata/config.yml") 244 | if err != nil { 245 | panic("failed to load config.yml from file") 246 | } 247 | assert.Equal(t, "9001", ConfGorush.Core.Port) 248 | assert.Equal(t, int64(200), ConfGorush.Core.MaxNotification) 249 | assert.True(t, ConfGorush.GRPC.Enabled) 250 | assert.Equal(t, "ABC123DEFG", ConfGorush.Ios.KeyID) 251 | assert.Equal(t, "DEF123GHIJ", ConfGorush.Ios.TeamID) 252 | assert.Equal(t, "/healthz", ConfGorush.API.HealthURI) 253 | assert.Equal(t, "http://example.com", ConfGorush.Core.FeedbackURL) 254 | assert.Equal(t, "x-api-key:1234567890", ConfGorush.Core.FeedbackHeader[0]) 255 | assert.Equal(t, "x-auth-key:0987654321", ConfGorush.Core.FeedbackHeader[1]) 256 | } 257 | 258 | func TestLoadWrongDefaultYAMLConfig(t *testing.T) { 259 | defaultConf = []byte(`a`) 260 | _, err := LoadConf() 261 | assert.Error(t, err) 262 | } 263 | -------------------------------------------------------------------------------- /config/testdata/config.yml: -------------------------------------------------------------------------------- 1 | core: 2 | enabled: true # enable httpd server 3 | address: "" # ip address to bind (default: any) 4 | shutdown_timeout: 30 # default is 30 second 5 | port: "8088" # ignore this port number if auto_tls is enabled (listen 443). 6 | worker_num: 0 # default worker number is runtime.NumCPU() 7 | queue_num: 0 # default queue number is 8192 8 | max_notification: 100 9 | # set true if you need get error message from fail push notification in API response. 10 | # It only works when the queue engine is local. 11 | sync: false 12 | # set webhook url if you need get error message asynchronously from fail push notification in API response. 13 | feedback_hook_url: "" 14 | feedback_timeout: 10 # default is 10 second 15 | feedback_header: 16 | - x-gorush-token:4e989115e09680f44a645519fed6a976 17 | mode: "release" 18 | ssl: false 19 | cert_path: "cert.pem" 20 | key_path: "key.pem" 21 | cert_base64: "" 22 | key_base64: "" 23 | http_proxy: "" 24 | pid: 25 | enabled: false 26 | path: "gorush.pid" 27 | override: true 28 | auto_tls: 29 | enabled: false # Automatically install TLS certificates from Let's Encrypt. 30 | folder: ".cache" # folder for storing TLS certificates 31 | host: "" # which domains the Let's Encrypt will attempt 32 | 33 | grpc: 34 | enabled: false # enable gRPC server 35 | port: 9000 36 | 37 | api: 38 | push_uri: "/api/push" 39 | stat_go_uri: "/api/stat/go" 40 | stat_app_uri: "/api/stat/app" 41 | config_uri: "/api/config" 42 | sys_stat_uri: "/sys/stats" 43 | metric_uri: "/metrics" 44 | health_uri: "/healthz" 45 | 46 | android: 47 | enabled: true 48 | key_path: "key.json" 49 | credential: "CREDENTIAL_JSON_DATA" 50 | max_retry: 0 # resend fail notification, default value zero is disabled 51 | 52 | huawei: 53 | enabled: false 54 | appsecret: "YOUR_APP_SECRET" 55 | appid: "YOUR_APP_ID" 56 | max_retry: 0 # resend fail notification, default value zero is disabled 57 | 58 | queue: 59 | engine: "local" # support "local", "nsq", "nats" and "redis" default value is "local" 60 | nsq: 61 | addr: 127.0.0.1:4150 62 | topic: gorush 63 | channel: gorush 64 | nats: 65 | addr: 127.0.0.1:4222 66 | subj: gorush 67 | queue: gorush 68 | redis: 69 | addr: 127.0.0.1:6379 70 | group: gorush 71 | consumer: gorush 72 | stream_name: gorush 73 | username: "" 74 | password: "" 75 | with_tls: false 76 | 77 | ios: 78 | enabled: false 79 | key_path: "key.pem" 80 | key_base64: "" # load iOS key from base64 input 81 | key_type: "pem" # could be pem, p12 or p8 type 82 | password: "" # certificate password, default as empty string. 83 | production: false 84 | max_concurrent_pushes: 100 # just for push ios notification 85 | max_retry: 0 # resend fail notification, default value zero is disabled 86 | key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) 87 | team_id: "" # TeamID from developer account (View Account -> Membership) 88 | 89 | log: 90 | format: "string" # string or json 91 | access_log: "stdout" # stdout: output to console, or define log path like "log/access_log" 92 | access_level: "debug" 93 | error_log: "stderr" # stderr: output to console, or define log path like "log/error_log" 94 | error_level: "error" 95 | hide_token: true 96 | hide_messages: false 97 | 98 | stat: 99 | engine: "memory" # support memory, redis, boltdb, buntdb or leveldb 100 | redis: 101 | cluster: false 102 | addr: "localhost:6379" # if cluster is true, you may set this to "localhost:6379,localhost:6380,localhost:6381" 103 | username: "" 104 | password: "" 105 | db: 0 106 | boltdb: 107 | path: "bolt.db" 108 | bucket: "gorush" 109 | buntdb: 110 | path: "bunt.db" 111 | leveldb: 112 | path: "level.db" 113 | badgerdb: 114 | path: "badger.db" 115 | -------------------------------------------------------------------------------- /config/testdata/empty.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gorush/316fa2605d16e50a32415560ab81d81b5a9bd1f9/config/testdata/empty.yml -------------------------------------------------------------------------------- /contrib/init/debian/README.md: -------------------------------------------------------------------------------- 1 | # Run gorush in Debian/Ubuntu 2 | 3 | ## Installation 4 | 5 | Put `gorush` binary into `/usr/bin` folder. 6 | 7 | ```sh 8 | cp gorush /usr/bin/ 9 | chmod +x /usr/bin/gorush 10 | ``` 11 | 12 | put `gorush` init script into `/etc/rc.d` 13 | 14 | ```sh 15 | cp contrib/init/debian/gorush /etc.rc.d/ 16 | ``` 17 | 18 | install and remove System-V style init script links 19 | 20 | ```sh 21 | update-rc.d gorush start 20 2 3 4 5 . stop 80 0 1 6 . 22 | ``` 23 | 24 | ## Start service 25 | 26 | create gorush configuration file. 27 | 28 | ```sh 29 | mkdir -p /etc/gorush 30 | cp config/testdata/config.yml /etc/gorush/ 31 | ``` 32 | 33 | start gorush service. 34 | 35 | ```sh 36 | /etc/init.d/gorush start 37 | ``` 38 | -------------------------------------------------------------------------------- /contrib/init/debian/gorush: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: gorush 5 | # Required-Start: $syslog $network 6 | # Required-Stop: $syslog $network 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: starts the gorush web server 10 | # Description: starts gorush using start-stop-daemon 11 | ### END INIT INFO 12 | 13 | # Original Author: Bo-Yi Wu (appleboy) 14 | 15 | # Do NOT "set -e" 16 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 17 | DESC="the gorush web server" 18 | NAME=gorush 19 | DAEMON=$(which gorush) 20 | 21 | DAEMONUSER=www-data 22 | PIDFILE=/var/run/$NAME.pid 23 | CONFIGFILE=/etc/gorush/config.yml 24 | DAEMONOPTS="-c $CONFIGFILE" 25 | 26 | USERBIND="setcap cap_net_bind_service=+ep" 27 | STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/5/KILL/5}" 28 | 29 | # Read configuration variable file if it is present 30 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 31 | 32 | # Exit if the package is not installed 33 | [ -x "$DAEMON" ] || exit 0 34 | 35 | # Set the ulimits 36 | ulimit -n 8192 37 | 38 | do_start() 39 | { 40 | $USERBIND $DAEMON 41 | sh -c "USER=$DAEMONUSER start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \\ 42 | --background --chuid $DAEMONUSER --exec $DAEMON -- $DAEMONOPTS" 43 | } 44 | 45 | do_stop() 46 | { 47 | start-stop-daemon --stop --quiet --retry=$STOP_SCHEDULE --pidfile $PIDFILE --name $NAME --oknodo 48 | rm -f $PIDFILE 49 | } 50 | 51 | do_status() 52 | { 53 | if [ -f $PIDFILE ]; then 54 | if kill -0 $(cat "$PIDFILE"); then 55 | echo "$NAME is running, PID is $(cat $PIDFILE)" 56 | else 57 | echo "$NAME process is dead, but pidfile exists" 58 | fi 59 | else 60 | echo "$NAME is not running" 61 | fi 62 | } 63 | 64 | case "$1" in 65 | start) 66 | echo "Starting $DESC" "$NAME" 67 | do_start 68 | ;; 69 | stop) 70 | echo "Stopping $DESC" "$NAME" 71 | do_stop 72 | ;; 73 | status) 74 | do_status 75 | ;; 76 | restart) 77 | echo "Restarting $DESC" "$NAME" 78 | do_stop 79 | do_start 80 | ;; 81 | *) 82 | echo "Usage: $SCRIPTNAME {start|stop|status|restart}" >&2 83 | exit 2 84 | ;; 85 | esac 86 | 87 | exit 0 88 | -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | const ( 4 | // PlatFormIos constant is 1 for iOS 5 | PlatFormIos = iota + 1 6 | // PlatFormAndroid constant is 2 for Android 7 | PlatFormAndroid 8 | // PlatFormHuawei constant is 3 for Huawei 9 | PlatFormHuawei 10 | ) 11 | 12 | const ( 13 | // SucceededPush is log block 14 | SucceededPush = "succeeded-push" 15 | // FailedPush is log block 16 | FailedPush = "failed-push" 17 | ) 18 | -------------------------------------------------------------------------------- /core/health.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "context" 4 | 5 | // Health defines a health-check connection. 6 | type Health interface { 7 | // Check returns if server is healthy or not 8 | Check(c context.Context) (bool, error) 9 | } 10 | -------------------------------------------------------------------------------- /core/queue.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // Queue as backend 4 | type Queue string 5 | 6 | var ( 7 | // LocalQueue for channel in Go 8 | LocalQueue Queue = "local" 9 | // NSQ a realtime distributed messaging platform 10 | NSQ Queue = "nsq" 11 | // NATS Connective Technology for Adaptive Edge & Distributed Systems 12 | NATS Queue = "nats" 13 | // Redis Pub/Sub 14 | Redis Queue = "redis" 15 | ) 16 | 17 | // IsLocalQueue check is Local Queue 18 | func IsLocalQueue(q Queue) bool { 19 | return q == LocalQueue 20 | } 21 | -------------------------------------------------------------------------------- /core/storage.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | const ( 4 | // TotalCountKey is key name for total count of storage 5 | TotalCountKey = "gorush-total-count" 6 | 7 | // IosSuccessKey is key name or ios success count of storage 8 | /* #nosec */ 9 | IosSuccessKey = "gorush-ios-success-count" 10 | 11 | // IosErrorKey is key name or ios success error of storage 12 | IosErrorKey = "gorush-ios-error-count" 13 | 14 | // AndroidSuccessKey is key name for android success count of storage 15 | AndroidSuccessKey = "gorush-android-success-count" 16 | 17 | // AndroidErrorKey is key name for android error count of storage 18 | AndroidErrorKey = "gorush-android-error-count" 19 | 20 | // HuaweiSuccessKey is key name for huawei success count of storage 21 | HuaweiSuccessKey = "gorush-huawei-success-count" 22 | 23 | // HuaweiErrorKey is key name for huawei error count of storage 24 | HuaweiErrorKey = "gorush-huawei-error-count" 25 | ) 26 | 27 | // Storage interface 28 | type Storage interface { 29 | Init() error 30 | Add(key string, count int64) 31 | Set(key string, count int64) 32 | Get(key string) int64 33 | Close() error 34 | } 35 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // A push notification server using Gin framework written in Go (Golang). 2 | // 3 | // Details about the gorush project are found in github page: 4 | // 5 | // https://github.com/appleboy/gorush 6 | // 7 | // The pre-compiled binaries can be downloaded from release page. 8 | // 9 | // Send Android notification 10 | // 11 | // $ gorush -android -m="your message" --fcm-key="FCM Key Path" -t="Device token" 12 | // 13 | // Send iOS notification 14 | // 15 | // $ gorush -ios -m="your message" -i="API Key" -t="Device token" 16 | // 17 | // The default endpoint is APNs development. Please add -production flag for APNs production push endpoint. 18 | // 19 | // $ gorush -ios -m="your message" -i="API Key" -t="Device token" -production 20 | // 21 | // Run gorush web server 22 | // 23 | // $ gorush -c config.yml 24 | // 25 | // Get go status of api server using httpie tool: 26 | // 27 | // $ http -v --verify=no --json GET https://localhost:8088/api/stat/go 28 | // 29 | // Simple send iOS notification example, the platform value is 1: 30 | // 31 | // { 32 | // "notifications": [ 33 | // { 34 | // "tokens": ["token_a", "token_b"], 35 | // "platform": 1, 36 | // "message": "Hello World iOS!" 37 | // } 38 | // ] 39 | // } 40 | // 41 | // Simple send Android notification example, the platform value is 2: 42 | // 43 | // { 44 | // "notifications": [ 45 | // { 46 | // "tokens": ["token_a", "token_b"], 47 | // "platform": 2, 48 | // "message": "Hello World Android!" 49 | // } 50 | // ] 51 | // } 52 | // 53 | // For more details, see the documentation and example. 54 | package main 55 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | gorush: 5 | image: appleboy/gorush 6 | restart: always 7 | ports: 8 | - "8088:8088" 9 | - "9000:9000" 10 | logging: 11 | options: 12 | max-size: "100k" 13 | max-file: "3" 14 | environment: 15 | - GORUSH_CORE_QUEUE_NUM=512 16 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | ARG USER=gorush 6 | ENV HOME /home/$USER 7 | 8 | LABEL maintainer="Bo-Yi Wu " \ 9 | org.label-schema.name="Gorush" \ 10 | org.label-schema.vendor="Bo-Yi Wu" \ 11 | org.label-schema.schema-version="1.0" 12 | 13 | # add new user 14 | RUN adduser -D $USER 15 | RUN apk add --no-cache ca-certificates mailcap && \ 16 | rm -rf /var/cache/apk/* 17 | 18 | COPY release/${TARGETOS}/${TARGETARCH}/gorush /bin/ 19 | 20 | USER $USER 21 | WORKDIR $HOME 22 | 23 | EXPOSE 8088 9000 24 | HEALTHCHECK --start-period=1s --interval=10s --timeout=5s \ 25 | CMD ["/bin/gorush", "--ping"] 26 | 27 | ENTRYPOINT ["/bin/gorush"] 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/appleboy/gorush 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | firebase.google.com/go/v4 v4.15.1 7 | github.com/apex/gateway v1.1.2 8 | github.com/appleboy/gin-status-api v1.1.0 9 | github.com/appleboy/go-fcm v1.2.2 10 | github.com/appleboy/go-hms-push v1.0.1 11 | github.com/appleboy/gofight/v2 v2.2.0 12 | github.com/appleboy/graceful v1.1.1 13 | github.com/asdine/storm/v3 v3.2.1 14 | github.com/buger/jsonparser v1.1.1 15 | github.com/dgraph-io/badger/v4 v4.2.0 16 | github.com/gin-contrib/logger v1.1.0 17 | github.com/gin-gonic/gin v1.10.0 18 | github.com/golang-queue/nats v0.2.0 19 | github.com/golang-queue/nsq v0.3.0 20 | github.com/golang-queue/queue v0.3.0 21 | github.com/golang-queue/redisdb-stream v0.3.0 22 | github.com/golang/protobuf v1.5.4 23 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 24 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 25 | github.com/json-iterator/go v1.1.12 26 | github.com/mattn/go-isatty v0.0.20 27 | github.com/mitchellh/mapstructure v1.5.0 28 | github.com/prometheus/client_golang v1.19.0 29 | github.com/redis/go-redis/v9 v9.7.3 30 | github.com/rs/zerolog v1.32.0 31 | github.com/sideshow/apns2 v0.25.0 32 | github.com/sirupsen/logrus v1.9.3 33 | github.com/spf13/viper v1.18.2 34 | github.com/stretchr/testify v1.10.0 35 | github.com/syndtr/goleveldb v1.0.0 36 | github.com/thoas/stats v0.0.0-20190407194641-965cb2de1678 37 | github.com/tidwall/buntdb v1.3.1 38 | go.opencensus.io v0.24.0 39 | go.uber.org/atomic v1.11.0 40 | golang.org/x/crypto v0.37.0 41 | golang.org/x/net v0.39.0 42 | golang.org/x/sync v0.13.0 43 | google.golang.org/grpc v1.68.0 44 | google.golang.org/protobuf v1.35.2 45 | ) 46 | 47 | require ( 48 | cel.dev/expr v0.18.0 // indirect 49 | cloud.google.com/go v0.116.0 // indirect 50 | cloud.google.com/go/auth v0.13.0 // indirect 51 | cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect 52 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 53 | cloud.google.com/go/firestore v1.17.0 // indirect 54 | cloud.google.com/go/iam v1.2.2 // indirect 55 | cloud.google.com/go/longrunning v0.6.3 // indirect 56 | cloud.google.com/go/monitoring v1.21.2 // indirect 57 | cloud.google.com/go/storage v1.47.0 // indirect 58 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect 59 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect 60 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect 61 | github.com/MicahParks/keyfunc v1.9.0 // indirect 62 | github.com/appleboy/com v0.2.1 // indirect 63 | github.com/aws/aws-lambda-go v1.46.0 // indirect 64 | github.com/beorn7/perks v1.0.1 // indirect 65 | github.com/bytedance/sonic v1.11.6 // indirect 66 | github.com/bytedance/sonic/loader v0.1.1 // indirect 67 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 68 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 69 | github.com/cloudwego/base64x v0.1.4 // indirect 70 | github.com/cloudwego/iasm v0.2.0 // indirect 71 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect 72 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 73 | github.com/dgraph-io/ristretto v0.1.1 // indirect 74 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 75 | github.com/dustin/go-humanize v1.0.1 // indirect 76 | github.com/envoyproxy/go-control-plane v0.13.1 // indirect 77 | github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect 78 | github.com/felixge/httpsnoop v1.0.4 // indirect 79 | github.com/fsnotify/fsnotify v1.7.0 // indirect 80 | github.com/fukata/golang-stats-api-handler v1.0.0 // indirect 81 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 82 | github.com/gin-contrib/sse v0.1.0 // indirect 83 | github.com/go-logr/logr v1.4.2 // indirect 84 | github.com/go-logr/stdr v1.2.2 // indirect 85 | github.com/go-playground/locales v0.14.1 // indirect 86 | github.com/go-playground/universal-translator v0.18.1 // indirect 87 | github.com/go-playground/validator/v10 v10.20.0 // indirect 88 | github.com/goccy/go-json v0.10.4 // indirect 89 | github.com/gogo/protobuf v1.3.2 // indirect 90 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 91 | github.com/golang/glog v1.2.4 // indirect 92 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 93 | github.com/golang/snappy v0.0.4 // indirect 94 | github.com/google/flatbuffers v24.3.7+incompatible // indirect 95 | github.com/google/s2a-go v0.1.8 // indirect 96 | github.com/google/uuid v1.6.0 // indirect 97 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 98 | github.com/googleapis/gax-go/v2 v2.14.0 // indirect 99 | github.com/hashicorp/hcl v1.0.0 // indirect 100 | github.com/jpillora/backoff v1.0.0 // indirect 101 | github.com/klauspost/compress v1.17.11 // indirect 102 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 103 | github.com/leodido/go-urn v1.4.0 // indirect 104 | github.com/magiconair/properties v1.8.7 // indirect 105 | github.com/mattn/go-colorable v0.1.13 // indirect 106 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 107 | github.com/modern-go/reflect2 v1.0.2 // indirect 108 | github.com/nats-io/nats.go v1.38.0 // indirect 109 | github.com/nats-io/nkeys v0.4.9 // indirect 110 | github.com/nats-io/nuid v1.0.1 // indirect 111 | github.com/nsqio/go-nsq v1.1.0 // indirect 112 | github.com/onsi/ginkgo v1.16.5 // indirect 113 | github.com/onsi/gomega v1.24.1 // indirect 114 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 115 | github.com/pkg/errors v0.9.1 // indirect 116 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 117 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 118 | github.com/prometheus/client_model v0.6.0 // indirect 119 | github.com/prometheus/common v0.50.0 // indirect 120 | github.com/prometheus/procfs v0.13.0 // indirect 121 | github.com/sagikazarmark/locafero v0.4.0 // indirect 122 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 123 | github.com/sourcegraph/conc v0.3.0 // indirect 124 | github.com/spf13/afero v1.11.0 // indirect 125 | github.com/spf13/cast v1.6.0 // indirect 126 | github.com/spf13/pflag v1.0.5 // indirect 127 | github.com/subosito/gotenv v1.6.0 // indirect 128 | github.com/tidwall/btree v1.7.0 // indirect 129 | github.com/tidwall/gjson v1.17.1 // indirect 130 | github.com/tidwall/grect v0.1.4 // indirect 131 | github.com/tidwall/match v1.1.1 // indirect 132 | github.com/tidwall/pretty v1.2.1 // indirect 133 | github.com/tidwall/rtred v0.1.2 // indirect 134 | github.com/tidwall/tinyqueue v0.1.1 // indirect 135 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 136 | github.com/ugorji/go/codec v1.2.12 // indirect 137 | go.etcd.io/bbolt v1.3.10 // indirect 138 | go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect 139 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 // indirect 140 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect 141 | go.opentelemetry.io/otel v1.32.0 // indirect 142 | go.opentelemetry.io/otel/metric v1.32.0 // indirect 143 | go.opentelemetry.io/otel/sdk v1.32.0 // indirect 144 | go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect 145 | go.opentelemetry.io/otel/trace v1.32.0 // indirect 146 | go.uber.org/multierr v1.11.0 // indirect 147 | golang.org/x/arch v0.8.0 // indirect 148 | golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect 149 | golang.org/x/oauth2 v0.25.0 // indirect 150 | golang.org/x/sys v0.32.0 // indirect 151 | golang.org/x/text v0.24.0 // indirect 152 | golang.org/x/time v0.8.0 // indirect 153 | google.golang.org/api v0.212.0 // indirect 154 | google.golang.org/appengine/v2 v2.0.6 // indirect 155 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect 156 | google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect 157 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect 158 | google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 // indirect 159 | gopkg.in/ini.v1 v1.67.0 // indirect 160 | gopkg.in/yaml.v3 v3.0.1 // indirect 161 | ) 162 | 163 | replace github.com/msalihkarakasli/go-hms-push => github.com/spawn2kill/go-hms-push v0.0.0-20211125124117-e20af53b1304 164 | -------------------------------------------------------------------------------- /helm/gorush/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm/gorush/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: gorush 3 | description: A push notification micro server using Gin framework written in Go (Golang) 4 | type: application 5 | version: 0.1.0 6 | appVersion: "1.14.0" 7 | dependencies: 8 | - name: redis 9 | version: ~14.1 10 | repository: https://charts.bitnami.com/bitnami 11 | condition: redis.enabled 12 | -------------------------------------------------------------------------------- /helm/gorush/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "gorush.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "gorush.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "gorush.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "gorush.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /helm/gorush/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "gorush.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "gorush.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "gorush.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "gorush.labels" -}} 37 | helm.sh/chart: {{ include "gorush.chart" . }} 38 | {{ include "gorush.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "gorush.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "gorush.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "gorush.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "gorush.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /helm/gorush/templates/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ .Chart.Name }} 5 | namespace: {{ .Chart.Name }} 6 | data: 7 | # stat 8 | stats: 9 | engine: {{ .Values.stat.engine }} 10 | {{- if .Values.redis.enabled }} 11 | redis: 12 | host: {{ .Values.redis.host }}:{{ .Values.redis.port }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /helm/gorush/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Chart.Name }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "gorush.labels" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.replicaCount }} 10 | selector: 11 | matchLabels: 12 | {{- include "gorush.selectorLabels" . | nindent 6 }} 13 | template: 14 | metadata: 15 | {{- with .Values.podAnnotations }} 16 | annotations: 17 | {{- toYaml . | nindent 8 }} 18 | {{- end }} 19 | labels: 20 | {{- include "gorush.selectorLabels" . | nindent 8 }} 21 | spec: 22 | {{- with .Values.imagePullSecrets }} 23 | imagePullSecrets: 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | serviceAccountName: {{ include "gorush.serviceAccountName" . }} 27 | securityContext: 28 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 29 | containers: 30 | - name: {{ .Chart.Name }} 31 | securityContext: 32 | {{- toYaml .Values.securityContext | nindent 12 }} 33 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 34 | imagePullPolicy: {{ .Values.image.pullPolicy }} 35 | ports: 36 | - name: http 37 | containerPort: {{ .Values.service.port }} 38 | protocol: TCP 39 | livenessProbe: 40 | httpGet: 41 | path: /healthz 42 | port: http 43 | initialDelaySeconds: 15 44 | periodSeconds: 15 45 | env: 46 | - name: GORUSH_STAT_ENGINE 47 | valueFrom: 48 | configMapKeyRef: 49 | name: {{ .Chart.Name }}-config 50 | key: stat.engine 51 | {{- if .Values.redis.enabled }} 52 | - name: GORUSH_STAT_REDIS_ADDR 53 | valueFrom: 54 | configMapKeyRef: 55 | name: {{ .Chart.Name }}-config 56 | key: stat.redis.host 57 | {{- end }} 58 | readinessProbe: 59 | httpGet: 60 | path: / 61 | port: http 62 | resources: 63 | {{- toYaml .Values.resources | nindent 12 }} 64 | {{- with .Values.nodeSelector }} 65 | nodeSelector: 66 | {{- toYaml . | nindent 8 }} 67 | {{- end }} 68 | {{- with .Values.affinity }} 69 | affinity: 70 | {{- toYaml . | nindent 8 }} 71 | {{- end }} 72 | {{- with .Values.tolerations }} 73 | tolerations: 74 | {{- toYaml . | nindent 8 }} 75 | {{- end }} 76 | -------------------------------------------------------------------------------- /helm/gorush/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ .Chart.Name }} 6 | namespace: {{ .Chart.Name }} 7 | labels: 8 | {{- include "gorush.labels" . | nindent 4 }} 9 | spec: 10 | scaleTargetRef: 11 | apiVersion: apps/v1 12 | kind: Deployment 13 | name: {{ .Chart.Name }} 14 | minReplicas: {{ .Values.autoscaling.minReplicas }} 15 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 16 | metrics: 17 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 18 | - type: Resource 19 | resource: 20 | name: cpu 21 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 22 | {{- end }} 23 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 24 | - type: Resource 25 | resource: 26 | name: memory 27 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 28 | {{- end }} 29 | {{- end }} 30 | -------------------------------------------------------------------------------- /helm/gorush/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $svcPort := .Values.service.port -}} 3 | {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 4 | apiVersion: networking.k8s.io/v1 5 | {{- else -}} 6 | apiVersion: extensions/v1beta1 7 | {{- end }} 8 | kind: Ingress 9 | metadata: 10 | name: {{ .Chart.Name }} 11 | namespace: {{ .Chart.Name }} 12 | labels: 13 | {{- include "gorush.labels" . | nindent 4 }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | {{- toYaml . | nindent 4 }} 17 | {{- end }} 18 | spec: 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | {{- range .Values.ingress.tls }} 22 | - hosts: 23 | {{- range .hosts }} 24 | - {{ . | quote }} 25 | {{- end }} 26 | secretName: {{ .secretName }} 27 | {{- end }} 28 | {{- end }} 29 | rules: 30 | {{- range .Values.ingress.hosts }} 31 | - host: {{ .host | quote }} 32 | http: 33 | paths: 34 | {{- range .paths }} 35 | - path: {{ .path }} 36 | pathType: Prefix 37 | backend: 38 | service: 39 | name: gorush 40 | port: 41 | number: {{ $svcPort }} 42 | {{- end }} 43 | {{- end }} 44 | {{- end }} 45 | -------------------------------------------------------------------------------- /helm/gorush/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Chart.Name }} 5 | namespace: {{ .Chart.Name }} 6 | labels: 7 | {{- include "gorush.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.service.type }} 10 | ports: 11 | - port: {{ .Values.service.port }} 12 | targetPort: http 13 | protocol: TCP 14 | name: http 15 | selector: 16 | {{- include "gorush.selectorLabels" . | nindent 4 }} 17 | -------------------------------------------------------------------------------- /helm/gorush/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "gorush.serviceAccountName" . }} 6 | namespace: {{ .Chart.Name }} 7 | labels: 8 | {{- include "gorush.labels" . | nindent 4 }} 9 | {{- with .Values.serviceAccount.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /helm/gorush/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for gorush. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: appleboy/gorush 9 | pullPolicy: Always 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: false 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: {} 29 | # fsGroup: 2000 30 | 31 | securityContext: {} 32 | # capabilities: 33 | # drop: 34 | # - ALL 35 | # readOnlyRootFilesystem: true 36 | # runAsNonRoot: true 37 | # runAsUser: 1000 38 | 39 | service: 40 | type: ClusterIP 41 | port: 8088 42 | 43 | stats: 44 | engine: memory 45 | 46 | redis: 47 | enabled: false 48 | host: redis 49 | port: 6379 50 | 51 | ingress: 52 | enabled: false 53 | annotations: {} 54 | hosts: 55 | - host: gorush.example.com 56 | paths: 57 | - path: / 58 | tls: [] 59 | 60 | resources: {} 61 | 62 | autoscaling: 63 | enabled: false 64 | minReplicas: 1 65 | maxReplicas: 10 66 | targetCPUUtilizationPercentage: 80 67 | # targetMemoryUtilizationPercentage: 80 68 | 69 | nodeSelector: {} 70 | 71 | tolerations: [] 72 | 73 | affinity: {} 74 | -------------------------------------------------------------------------------- /k8s/gorush-aws-alb-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: gorush 5 | namespace: gorush 6 | annotations: 7 | # Kubernetes Ingress Controller for AWS ALB 8 | # https://github.com/coreos/alb-ingress-controller 9 | alb.ingress.kubernetes.io/scheme: internet-facing 10 | alb.ingress.kubernetes.io/subnets: subnet-aa3dfbe3,subnet-4aff342d 11 | alb.ingress.kubernetes.io/security-groups: sg-71069b17 12 | spec: 13 | rules: 14 | - host: gorush.example.com 15 | http: 16 | paths: 17 | - path: / 18 | backend: 19 | serviceName: gorush 20 | servicePort: 8088 21 | -------------------------------------------------------------------------------- /k8s/gorush-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: gorush-config 5 | namespace: gorush 6 | data: 7 | # stat 8 | stat.engine: redis 9 | stat.redis.host: redis:6379 10 | -------------------------------------------------------------------------------- /k8s/gorush-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: gorush 5 | namespace: gorush 6 | spec: 7 | replicas: 2 8 | selector: 9 | matchLabels: 10 | app: gorush 11 | tier: frontend 12 | template: 13 | metadata: 14 | labels: 15 | app: gorush 16 | tier: frontend 17 | spec: 18 | containers: 19 | - image: appleboy/gorush:1.18.4 20 | name: gorush 21 | imagePullPolicy: Always 22 | ports: 23 | - containerPort: 8088 24 | resources: 25 | requests: 26 | cpu: "250m" 27 | memory: "256Mi" 28 | limits: 29 | cpu: "500m" 30 | memory: "512Mi" 31 | livenessProbe: 32 | httpGet: 33 | path: /healthz 34 | port: 8000 35 | initialDelaySeconds: 3 36 | periodSeconds: 3 37 | env: 38 | - name: GORUSH_STAT_ENGINE 39 | valueFrom: 40 | configMapKeyRef: 41 | name: gorush-config 42 | key: stat.engine 43 | - name: GORUSH_STAT_REDIS_ADDR 44 | valueFrom: 45 | configMapKeyRef: 46 | name: gorush-config 47 | key: stat.redis.host 48 | - name: GORUSH_CORE_PORT 49 | value: "8000" 50 | -------------------------------------------------------------------------------- /k8s/gorush-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: gorush 5 | -------------------------------------------------------------------------------- /k8s/gorush-redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: redis 5 | namespace: gorush 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: redis 10 | role: master 11 | tier: backend 12 | replicas: 1 13 | template: 14 | metadata: 15 | labels: 16 | app: redis 17 | role: master 18 | tier: backend 19 | spec: 20 | containers: 21 | - name: master 22 | image: redis:7 23 | ports: 24 | - containerPort: 6379 25 | resources: 26 | requests: 27 | cpu: "250m" 28 | memory: "256Mi" 29 | limits: 30 | cpu: "500m" 31 | memory: "512Mi" 32 | -------------------------------------------------------------------------------- /k8s/gorush-redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: redis 5 | namespace: gorush 6 | labels: 7 | app: redis 8 | role: master 9 | tier: backend 10 | spec: 11 | ports: 12 | - port: 6379 13 | targetPort: 6379 14 | selector: 15 | app: redis 16 | role: master 17 | tier: backend 18 | -------------------------------------------------------------------------------- /k8s/gorush-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: gorush 5 | namespace: gorush 6 | labels: 7 | app: gorush 8 | tier: frontend 9 | spec: 10 | selector: 11 | app: gorush 12 | tier: frontend 13 | # if your cluster supports it, uncomment the following to automatically create 14 | # an external load-balanced IP for the frontend service. 15 | # type: LoadBalancer 16 | # 17 | # if you want to expose the service to the outside (without a load balancer in front) 18 | # type: NodePort 19 | # 20 | # if you want gorush to be accessible only within the cluster 21 | # type: ClusterIP 22 | ports: 23 | - protocol: TCP 24 | port: 80 25 | targetPort: 8088 26 | -------------------------------------------------------------------------------- /logx/log.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/appleboy/gorush/core" 11 | 12 | "github.com/mattn/go-isatty" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var ( 17 | green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) 18 | yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) 19 | red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) 20 | blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) 21 | reset = string([]byte{27, 91, 48, 109}) 22 | ) 23 | 24 | // LogPushEntry is push response log 25 | type LogPushEntry struct { 26 | ID string `json:"notif_id,omitempty"` 27 | Type string `json:"type"` 28 | Platform string `json:"platform"` 29 | Token string `json:"token"` 30 | Message string `json:"message"` 31 | Error string `json:"error"` 32 | } 33 | 34 | var isTerm bool 35 | 36 | //nolint 37 | func init() { 38 | isTerm = isatty.IsTerminal(os.Stdout.Fd()) 39 | } 40 | 41 | var ( 42 | // LogAccess is log server request log 43 | LogAccess = logrus.New() 44 | // LogError is log server error log 45 | LogError = logrus.New() 46 | ) 47 | 48 | // InitLog use for initial log module 49 | func InitLog(accessLevel, accessLog, errorLevel, errorLog string) error { 50 | var err error 51 | 52 | if !isTerm { 53 | LogAccess.SetFormatter(&logrus.JSONFormatter{}) 54 | LogError.SetFormatter(&logrus.JSONFormatter{}) 55 | } else { 56 | LogAccess.Formatter = &logrus.TextFormatter{ 57 | TimestampFormat: "2006/01/02 - 15:04:05", 58 | FullTimestamp: true, 59 | } 60 | 61 | LogError.Formatter = &logrus.TextFormatter{ 62 | TimestampFormat: "2006/01/02 - 15:04:05", 63 | FullTimestamp: true, 64 | } 65 | } 66 | 67 | // set logger 68 | if err = SetLogLevel(LogAccess, accessLevel); err != nil { 69 | return errors.New("Set access log level error: " + err.Error()) 70 | } 71 | 72 | if err = SetLogLevel(LogError, errorLevel); err != nil { 73 | return errors.New("Set error log level error: " + err.Error()) 74 | } 75 | 76 | if err = SetLogOut(LogAccess, accessLog); err != nil { 77 | return errors.New("Set access log path error: " + err.Error()) 78 | } 79 | 80 | if err = SetLogOut(LogError, errorLog); err != nil { 81 | return errors.New("Set error log path error: " + err.Error()) 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // SetLogOut provide log stdout and stderr output 88 | func SetLogOut(log *logrus.Logger, outString string) error { 89 | switch outString { 90 | case "stdout": 91 | log.Out = os.Stdout 92 | case "stderr": 93 | log.Out = os.Stderr 94 | default: 95 | f, err := os.OpenFile(outString, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | log.Out = f 101 | } 102 | 103 | return nil 104 | } 105 | 106 | // SetLogLevel is define log level what you want 107 | // log level: panic, fatal, error, warn, info and debug 108 | func SetLogLevel(log *logrus.Logger, levelString string) error { 109 | level, err := logrus.ParseLevel(levelString) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | log.Level = level 115 | 116 | return nil 117 | } 118 | 119 | func colorForPlatForm(platform int) string { 120 | switch platform { 121 | case core.PlatFormIos: 122 | return blue 123 | case core.PlatFormAndroid: 124 | return yellow 125 | case core.PlatFormHuawei: 126 | return green 127 | default: 128 | return reset 129 | } 130 | } 131 | 132 | func typeForPlatForm(platform int) string { 133 | switch platform { 134 | case core.PlatFormIos: 135 | return "ios" 136 | case core.PlatFormAndroid: 137 | return "android" 138 | case core.PlatFormHuawei: 139 | return "huawei" 140 | default: 141 | return "" 142 | } 143 | } 144 | 145 | func hideToken(token string, markLen int) string { 146 | if token == "" { 147 | return "" 148 | } 149 | 150 | if len(token) < markLen*2 { 151 | return strings.Repeat("*", len(token)) 152 | } 153 | 154 | start := token[len(token)-markLen:] 155 | end := token[0:markLen] 156 | 157 | result := strings.ReplaceAll(token, start, strings.Repeat("*", markLen)) 158 | result = strings.ReplaceAll(result, end, strings.Repeat("*", markLen)) 159 | 160 | return result 161 | } 162 | 163 | // GetLogPushEntry get push data into log structure 164 | func GetLogPushEntry(input *InputLog) LogPushEntry { 165 | var errMsg string 166 | 167 | plat := typeForPlatForm(input.Platform) 168 | 169 | if input.Error != nil { 170 | errMsg = input.Error.Error() 171 | } 172 | 173 | token := input.Token 174 | if input.HideToken { 175 | token = hideToken(input.Token, 10) 176 | } 177 | 178 | message := input.Message 179 | if input.HideMessage { 180 | message = "(message redacted)" 181 | } 182 | 183 | return LogPushEntry{ 184 | ID: input.ID, 185 | Type: input.Status, 186 | Platform: plat, 187 | Token: token, 188 | Message: message, 189 | Error: errMsg, 190 | } 191 | } 192 | 193 | // InputLog log request 194 | type InputLog struct { 195 | ID string 196 | Status string 197 | Token string 198 | Message string 199 | Platform int 200 | Error error 201 | HideToken bool 202 | HideMessage bool 203 | Format string 204 | } 205 | 206 | // LogPush record user push request and server response. 207 | func LogPush(input *InputLog) LogPushEntry { 208 | var platColor, resetColor, output string 209 | 210 | if isTerm { 211 | platColor = colorForPlatForm(input.Platform) 212 | resetColor = reset 213 | } 214 | 215 | log := GetLogPushEntry(input) 216 | 217 | if input.Format == "json" { 218 | logJSON, _ := json.Marshal(log) 219 | 220 | output = string(logJSON) 221 | } else { 222 | var typeColor string 223 | switch input.Status { 224 | case core.SucceededPush: 225 | if isTerm { 226 | typeColor = green 227 | } 228 | 229 | output = fmt.Sprintf("|%s %s %s| %s%s%s [%s] %s", 230 | typeColor, log.Type, resetColor, 231 | platColor, log.Platform, resetColor, 232 | log.Token, 233 | log.Message, 234 | ) 235 | case core.FailedPush: 236 | if isTerm { 237 | typeColor = red 238 | } 239 | 240 | output = fmt.Sprintf("|%s %s %s| %s%s%s [%s] | %s | Error Message: %s", 241 | typeColor, log.Type, resetColor, 242 | platColor, log.Platform, resetColor, 243 | log.Token, 244 | log.Message, 245 | log.Error, 246 | ) 247 | } 248 | } 249 | 250 | switch input.Status { 251 | case core.SucceededPush: 252 | LogAccess.Info(output) 253 | case core.FailedPush: 254 | LogError.Error(output) 255 | } 256 | 257 | return log 258 | } 259 | -------------------------------------------------------------------------------- /logx/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gorush/316fa2605d16e50a32415560ab81d81b5a9bd1f9/logx/log/.gitkeep -------------------------------------------------------------------------------- /logx/log/access.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gorush/316fa2605d16e50a32415560ab81d81b5a9bd1f9/logx/log/access.log -------------------------------------------------------------------------------- /logx/log_interface.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // QueueLogger for simple logger. 10 | func QueueLogger() DefaultQueueLogger { 11 | return DefaultQueueLogger{ 12 | accessLogger: LogAccess, 13 | errorLogger: LogError, 14 | } 15 | } 16 | 17 | // DefaultQueueLogger for queue custom logger 18 | type DefaultQueueLogger struct { 19 | accessLogger *logrus.Logger 20 | errorLogger *logrus.Logger 21 | } 22 | 23 | func (l DefaultQueueLogger) Infof(format string, args ...interface{}) { 24 | l.accessLogger.Printf(format, args...) 25 | } 26 | 27 | func (l DefaultQueueLogger) Errorf(format string, args ...interface{}) { 28 | l.errorLogger.Printf(format, args...) 29 | } 30 | 31 | func (l DefaultQueueLogger) Fatalf(format string, args ...interface{}) { 32 | l.errorLogger.Fatalf(format, args...) 33 | } 34 | 35 | func (l DefaultQueueLogger) Info(args ...interface{}) { 36 | l.accessLogger.Println(fmt.Sprint(args...)) 37 | } 38 | 39 | func (l DefaultQueueLogger) Error(args ...interface{}) { 40 | l.errorLogger.Println(fmt.Sprint(args...)) 41 | } 42 | 43 | func (l DefaultQueueLogger) Fatal(args ...interface{}) { 44 | l.errorLogger.Println(fmt.Sprint(args...)) 45 | } 46 | -------------------------------------------------------------------------------- /logx/log_test.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/appleboy/gorush/config" 8 | "github.com/appleboy/gorush/core" 9 | 10 | "github.com/sirupsen/logrus" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var invalidLevel = "invalid" 15 | 16 | func TestSetLogLevel(t *testing.T) { 17 | log := logrus.New() 18 | 19 | err := SetLogLevel(log, "debug") 20 | assert.Nil(t, err) 21 | 22 | err = SetLogLevel(log, invalidLevel) 23 | assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error()) 24 | } 25 | 26 | func TestSetLogOut(t *testing.T) { 27 | log := logrus.New() 28 | 29 | err := SetLogOut(log, "stdout") 30 | assert.Nil(t, err) 31 | 32 | err = SetLogOut(log, "stderr") 33 | assert.Nil(t, err) 34 | 35 | err = SetLogOut(log, "log/access.log") 36 | assert.Nil(t, err) 37 | 38 | // missing create logs folder. 39 | err = SetLogOut(log, "logs/access.log") 40 | assert.NotNil(t, err) 41 | } 42 | 43 | func TestInitDefaultLog(t *testing.T) { 44 | cfg, _ := config.LoadConf() 45 | 46 | // no errors on default config 47 | assert.Nil(t, InitLog( 48 | cfg.Log.AccessLevel, 49 | cfg.Log.AccessLog, 50 | cfg.Log.ErrorLevel, 51 | cfg.Log.ErrorLog, 52 | )) 53 | 54 | cfg.Log.AccessLevel = invalidLevel 55 | 56 | assert.NotNil(t, InitLog( 57 | cfg.Log.AccessLevel, 58 | cfg.Log.AccessLog, 59 | cfg.Log.ErrorLevel, 60 | cfg.Log.ErrorLog, 61 | )) 62 | 63 | isTerm = true 64 | 65 | assert.NotNil(t, InitLog( 66 | cfg.Log.AccessLevel, 67 | cfg.Log.AccessLog, 68 | cfg.Log.ErrorLevel, 69 | cfg.Log.ErrorLog, 70 | )) 71 | } 72 | 73 | func TestAccessLevel(t *testing.T) { 74 | cfg, _ := config.LoadConf() 75 | 76 | cfg.Log.AccessLevel = invalidLevel 77 | 78 | assert.NotNil(t, InitLog( 79 | cfg.Log.AccessLevel, 80 | cfg.Log.AccessLog, 81 | cfg.Log.ErrorLevel, 82 | cfg.Log.ErrorLog, 83 | )) 84 | } 85 | 86 | func TestErrorLevel(t *testing.T) { 87 | cfg, _ := config.LoadConf() 88 | 89 | cfg.Log.ErrorLevel = invalidLevel 90 | 91 | assert.NotNil(t, InitLog( 92 | cfg.Log.AccessLevel, 93 | cfg.Log.AccessLog, 94 | cfg.Log.ErrorLevel, 95 | cfg.Log.ErrorLog, 96 | )) 97 | } 98 | 99 | func TestAccessLogPath(t *testing.T) { 100 | cfg, _ := config.LoadConf() 101 | 102 | cfg.Log.AccessLog = "logs/access.log" 103 | 104 | assert.NotNil(t, InitLog( 105 | cfg.Log.AccessLevel, 106 | cfg.Log.AccessLog, 107 | cfg.Log.ErrorLevel, 108 | cfg.Log.ErrorLog, 109 | )) 110 | } 111 | 112 | func TestErrorLogPath(t *testing.T) { 113 | cfg, _ := config.LoadConf() 114 | 115 | cfg.Log.ErrorLog = "logs/error.log" 116 | 117 | assert.NotNil(t, InitLog( 118 | cfg.Log.AccessLevel, 119 | cfg.Log.AccessLog, 120 | cfg.Log.ErrorLevel, 121 | cfg.Log.ErrorLog, 122 | )) 123 | } 124 | 125 | func TestPlatFormType(t *testing.T) { 126 | assert.Equal(t, "ios", typeForPlatForm(core.PlatFormIos)) 127 | assert.Equal(t, "android", typeForPlatForm(core.PlatFormAndroid)) 128 | assert.Equal(t, "huawei", typeForPlatForm(core.PlatFormHuawei)) 129 | assert.Equal(t, "", typeForPlatForm(10000)) 130 | } 131 | 132 | func TestPlatFormColor(t *testing.T) { 133 | assert.Equal(t, blue, colorForPlatForm(core.PlatFormIos)) 134 | assert.Equal(t, yellow, colorForPlatForm(core.PlatFormAndroid)) 135 | assert.Equal(t, green, colorForPlatForm(core.PlatFormHuawei)) 136 | assert.Equal(t, reset, colorForPlatForm(1000000)) 137 | } 138 | 139 | func TestHideToken(t *testing.T) { 140 | assert.Equal(t, "", hideToken("", 2)) 141 | assert.Equal(t, "**345678**", hideToken("1234567890", 2)) 142 | assert.Equal(t, "*****", hideToken("12345", 10)) 143 | } 144 | 145 | func TestLogPushEntry(t *testing.T) { 146 | in := InputLog{} 147 | 148 | in.Platform = 1 149 | assert.Equal(t, "ios", GetLogPushEntry(&in).Platform) 150 | 151 | in.Error = errors.New("error") 152 | assert.Equal(t, "error", GetLogPushEntry(&in).Error) 153 | 154 | in.Token = "1234567890" 155 | in.HideToken = true 156 | assert.Equal(t, "**********", GetLogPushEntry(&in).Token) 157 | 158 | in.Message = "hellothisisamessage" 159 | in.HideMessage = true 160 | assert.Equal(t, "(message redacted)", GetLogPushEntry(&in).Message) 161 | } 162 | 163 | func TestLogPush(t *testing.T) { 164 | in := InputLog{} 165 | isTerm = true 166 | 167 | in.Format = "json" 168 | in.Status = "succeeded-push" 169 | assert.Equal(t, "succeeded-push", LogPush(&in).Type) 170 | 171 | in.Format = "" 172 | in.Message = "success" 173 | assert.Equal(t, "success", LogPush(&in).Message) 174 | 175 | in.Status = "failed-push" 176 | in.Message = "failed" 177 | assert.Equal(t, "failed", LogPush(&in).Message) 178 | } 179 | -------------------------------------------------------------------------------- /metric/metrics.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "github.com/appleboy/gorush/status" 5 | 6 | "github.com/golang-queue/queue" 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | const namespace = "gorush_" 11 | 12 | // Metrics implements the prometheus.Metrics interface and 13 | // exposes gorush metrics for prometheus 14 | type Metrics struct { 15 | TotalPushCount *prometheus.Desc 16 | IosSuccess *prometheus.Desc 17 | IosError *prometheus.Desc 18 | AndroidSuccess *prometheus.Desc 19 | AndroidError *prometheus.Desc 20 | HuaweiSuccess *prometheus.Desc 21 | HuaweiError *prometheus.Desc 22 | BusyWorkers *prometheus.Desc 23 | SuccessTasks *prometheus.Desc 24 | FailureTasks *prometheus.Desc 25 | SubmittedTasks *prometheus.Desc 26 | q *queue.Queue 27 | } 28 | 29 | // NewMetrics returns a new Metrics with all prometheus.Desc initialized 30 | func NewMetrics(q *queue.Queue) Metrics { 31 | m := Metrics{ 32 | TotalPushCount: prometheus.NewDesc( 33 | namespace+"total_push_count", 34 | "Number of push count", 35 | nil, nil, 36 | ), 37 | IosSuccess: prometheus.NewDesc( 38 | namespace+"ios_success", 39 | "Number of iOS success count", 40 | nil, nil, 41 | ), 42 | IosError: prometheus.NewDesc( 43 | namespace+"ios_error", 44 | "Number of iOS fail count", 45 | nil, nil, 46 | ), 47 | AndroidSuccess: prometheus.NewDesc( 48 | namespace+"android_success", 49 | "Number of android success count", 50 | nil, nil, 51 | ), 52 | AndroidError: prometheus.NewDesc( 53 | namespace+"android_fail", 54 | "Number of android fail count", 55 | nil, nil, 56 | ), 57 | HuaweiSuccess: prometheus.NewDesc( 58 | namespace+"huawei_success", 59 | "Number of huawei success count", 60 | nil, nil, 61 | ), 62 | HuaweiError: prometheus.NewDesc( 63 | namespace+"huawei_fail", 64 | "Number of huawei fail count", 65 | nil, nil, 66 | ), 67 | BusyWorkers: prometheus.NewDesc( 68 | namespace+"busy_workers", 69 | "Length of busy workers", 70 | nil, nil, 71 | ), 72 | FailureTasks: prometheus.NewDesc( 73 | namespace+"failure_tasks", 74 | "Length of Failure Tasks", 75 | nil, nil, 76 | ), 77 | SuccessTasks: prometheus.NewDesc( 78 | namespace+"success_tasks", 79 | "Length of Success Tasks", 80 | nil, nil, 81 | ), 82 | SubmittedTasks: prometheus.NewDesc( 83 | namespace+"submitted_tasks", 84 | "Length of Submitted Tasks", 85 | nil, nil, 86 | ), 87 | q: q, 88 | } 89 | 90 | return m 91 | } 92 | 93 | // Describe returns all possible prometheus.Desc 94 | func (c Metrics) Describe(ch chan<- *prometheus.Desc) { 95 | ch <- c.TotalPushCount 96 | ch <- c.IosSuccess 97 | ch <- c.IosError 98 | ch <- c.AndroidSuccess 99 | ch <- c.AndroidError 100 | ch <- c.HuaweiSuccess 101 | ch <- c.HuaweiError 102 | ch <- c.BusyWorkers 103 | ch <- c.SuccessTasks 104 | ch <- c.FailureTasks 105 | ch <- c.SubmittedTasks 106 | } 107 | 108 | // Collect returns the metrics with values 109 | func (c Metrics) Collect(ch chan<- prometheus.Metric) { 110 | ch <- prometheus.MustNewConstMetric( 111 | c.TotalPushCount, 112 | prometheus.CounterValue, 113 | float64(status.StatStorage.GetTotalCount()), 114 | ) 115 | ch <- prometheus.MustNewConstMetric( 116 | c.IosSuccess, 117 | prometheus.CounterValue, 118 | float64(status.StatStorage.GetIosSuccess()), 119 | ) 120 | ch <- prometheus.MustNewConstMetric( 121 | c.IosError, 122 | prometheus.CounterValue, 123 | float64(status.StatStorage.GetIosError()), 124 | ) 125 | ch <- prometheus.MustNewConstMetric( 126 | c.AndroidSuccess, 127 | prometheus.CounterValue, 128 | float64(status.StatStorage.GetAndroidSuccess()), 129 | ) 130 | ch <- prometheus.MustNewConstMetric( 131 | c.AndroidError, 132 | prometheus.CounterValue, 133 | float64(status.StatStorage.GetAndroidError()), 134 | ) 135 | ch <- prometheus.MustNewConstMetric( 136 | c.HuaweiSuccess, 137 | prometheus.CounterValue, 138 | float64(status.StatStorage.GetHuaweiSuccess()), 139 | ) 140 | ch <- prometheus.MustNewConstMetric( 141 | c.HuaweiError, 142 | prometheus.CounterValue, 143 | float64(status.StatStorage.GetHuaweiError()), 144 | ) 145 | ch <- prometheus.MustNewConstMetric( 146 | c.BusyWorkers, 147 | prometheus.GaugeValue, 148 | float64(c.q.BusyWorkers()), 149 | ) 150 | ch <- prometheus.MustNewConstMetric( 151 | c.SuccessTasks, 152 | prometheus.CounterValue, 153 | float64(c.q.SuccessTasks()), 154 | ) 155 | ch <- prometheus.MustNewConstMetric( 156 | c.FailureTasks, 157 | prometheus.CounterValue, 158 | float64(c.q.FailureTasks()), 159 | ) 160 | ch <- prometheus.MustNewConstMetric( 161 | c.SubmittedTasks, 162 | prometheus.CounterValue, 163 | float64(c.q.SubmittedTasks()), 164 | ) 165 | } 166 | -------------------------------------------------------------------------------- /metric/metrics_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/golang-queue/queue" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var noTask = func(ctx context.Context) error { return nil } 13 | 14 | func TestNewMetrics(t *testing.T) { 15 | q := queue.NewPool(10) 16 | assert.NoError(t, q.QueueTask(noTask)) 17 | assert.NoError(t, q.QueueTask(noTask)) 18 | time.Sleep(10 * time.Millisecond) 19 | defer q.Release() 20 | m := NewMetrics(q) 21 | assert.Equal(t, uint64(2), m.q.SubmittedTasks()) 22 | assert.Equal(t, uint64(2), m.q.SuccessTasks()) 23 | } 24 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "make build_linux_lambda" 3 | functions = "release/linux/lambda" 4 | 5 | [build.environment] 6 | GO111MODULE = "on" 7 | GO_IMPORT_PATH = "github.com/appleboy/gorush" 8 | GO_VERSION = "1.23.8" 9 | 10 | [[redirects]] 11 | from = "/*" 12 | status = 200 13 | to = "/.netlify/functions/gorush/:splat" 14 | -------------------------------------------------------------------------------- /notify/feedback.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/appleboy/gorush/logx" 12 | ) 13 | 14 | // extractHeaders converts a slice of strings to a map of strings. 15 | func extractHeaders(headers []string) map[string]string { 16 | result := make(map[string]string) 17 | for _, header := range headers { 18 | parts := strings.Split(header, ":") 19 | if len(parts) == 2 { 20 | result[parts[0]] = parts[1] 21 | } 22 | } 23 | return result 24 | } 25 | 26 | // DispatchFeedback sends a feedback log entry to a specified URL via an HTTP POST request. 27 | // 28 | // Parameters: 29 | // - ctx: The context for the HTTP request. 30 | // - log: The log entry to be sent as feedback. 31 | // - url: The destination URL for the feedback. 32 | // - timeout: The timeout duration for the HTTP request in seconds. 33 | // - header: A slice of strings representing additional headers to be included in the request. 34 | // 35 | // Returns: 36 | // - error: An error if the request fails or the response status is not OK. 37 | func DispatchFeedback(ctx context.Context, log logx.LogPushEntry, url string, timeout int64, header []string) error { 38 | if url == "" { 39 | return errors.New("url can't be empty") 40 | } 41 | 42 | payload, err := json.Marshal(log) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) 48 | defer cancel() 49 | req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(payload)) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | headers := extractHeaders(header) 55 | for k, v := range headers { 56 | req.Header.Set(k, strings.TrimSpace(v)) 57 | } 58 | 59 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 60 | 61 | feedbackClient.Timeout = time.Duration(timeout) * time.Second 62 | resp, err := feedbackClient.Do(req) 63 | 64 | if resp != nil { 65 | defer resp.Body.Close() 66 | } 67 | 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if resp.StatusCode != http.StatusOK { 73 | return errors.New("failed to send feedback") 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /notify/feedback_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/appleboy/gorush/config" 11 | "github.com/appleboy/gorush/logx" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestEmptyFeedbackURL(t *testing.T) { 17 | cfg, _ := config.LoadConf() 18 | logEntry := logx.LogPushEntry{ 19 | ID: "", 20 | Type: "", 21 | Platform: "", 22 | Token: "", 23 | Message: "", 24 | Error: "", 25 | } 26 | 27 | err := DispatchFeedback( 28 | context.Background(), 29 | logEntry, 30 | cfg.Core.FeedbackURL, 31 | cfg.Core.FeedbackTimeout, 32 | cfg.Core.FeedbackHeader, 33 | ) 34 | assert.NotNil(t, err) 35 | } 36 | 37 | func TestHTTPErrorInFeedbackCall(t *testing.T) { 38 | cfg, _ := config.LoadConf() 39 | cfg.Core.FeedbackURL = "http://test.example.com/api/" 40 | logEntry := logx.LogPushEntry{ 41 | ID: "", 42 | Type: "", 43 | Platform: "", 44 | Token: "", 45 | Message: "", 46 | Error: "", 47 | } 48 | 49 | err := DispatchFeedback( 50 | context.Background(), 51 | logEntry, 52 | cfg.Core.FeedbackURL, 53 | cfg.Core.FeedbackTimeout, 54 | cfg.Core.FeedbackHeader, 55 | ) 56 | assert.NotNil(t, err) 57 | } 58 | 59 | func TestSuccessfulFeedbackCall(t *testing.T) { 60 | // Mock http server 61 | httpMock := httptest.NewServer( 62 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 63 | if r.URL.Path == "/dispatch" { 64 | // check http header 65 | if r.Header.Get("x-gorush-token") != "1234" { 66 | panic("x-gorush-token header is not set") 67 | } 68 | 69 | w.Header().Add("Content-Type", "application/json") 70 | _, err := w.Write([]byte(`{}`)) 71 | if err != nil { 72 | log.Println(err) 73 | panic(err) 74 | } 75 | } 76 | }), 77 | ) 78 | defer httpMock.Close() 79 | 80 | cfg, _ := config.LoadConf() 81 | cfg.Core.FeedbackURL = httpMock.URL + "/dispatch" 82 | cfg.Core.FeedbackHeader = []string{ 83 | "x-gorush-token: 1234", 84 | } 85 | logEntry := logx.LogPushEntry{ 86 | ID: "", 87 | Type: "", 88 | Platform: "", 89 | Token: "", 90 | Message: "", 91 | Error: "", 92 | } 93 | 94 | err := DispatchFeedback( 95 | context.Background(), 96 | logEntry, 97 | cfg.Core.FeedbackURL, 98 | cfg.Core.FeedbackTimeout, 99 | cfg.Core.FeedbackHeader, 100 | ) 101 | assert.Nil(t, err) 102 | } 103 | -------------------------------------------------------------------------------- /notify/global.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/appleboy/go-fcm" 9 | "github.com/appleboy/go-hms-push/push/core" 10 | "github.com/sideshow/apns2" 11 | ) 12 | 13 | var ( 14 | // ApnsClient is apns client 15 | ApnsClient *apns2.Client 16 | // FCMClient is apns client 17 | FCMClient *fcm.Client 18 | // HMSClient is Huawei push client 19 | HMSClient *core.HMSClient 20 | // MaxConcurrentIOSPushes pool to limit the number of concurrent iOS pushes 21 | MaxConcurrentIOSPushes chan struct{} 22 | 23 | transport = &http.Transport{ 24 | Dial: (&net.Dialer{ 25 | Timeout: 5 * time.Second, 26 | }).Dial, 27 | TLSHandshakeTimeout: 5 * time.Second, 28 | MaxIdleConns: 5, 29 | MaxIdleConnsPerHost: 5, 30 | MaxConnsPerHost: 20, 31 | Proxy: http.ProxyFromEnvironment, // Support proxy 32 | } 33 | feedbackClient = &http.Client{ 34 | Transport: transport, 35 | } 36 | ) 37 | 38 | const ( 39 | HIGH = "high" 40 | NORMAL = "nornal" 41 | ) 42 | -------------------------------------------------------------------------------- /notify/main_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | 8 | "github.com/appleboy/gorush/config" 9 | "github.com/appleboy/gorush/status" 10 | ) 11 | 12 | func TestMain(m *testing.M) { 13 | cfg, _ := config.LoadConf() 14 | if err := status.InitAppStatus(cfg); err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | os.Exit(m.Run()) 19 | } 20 | -------------------------------------------------------------------------------- /notify/notification.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/appleboy/gorush/config" 11 | "github.com/appleboy/gorush/core" 12 | "github.com/appleboy/gorush/logx" 13 | 14 | "firebase.google.com/go/v4/messaging" 15 | "github.com/appleboy/go-hms-push/push/model" 16 | qcore "github.com/golang-queue/queue/core" 17 | jsoniter "github.com/json-iterator/go" 18 | ) 19 | 20 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 21 | 22 | // D provide string array 23 | type D map[string]interface{} 24 | 25 | const ( 26 | // ApnsPriorityLow will tell APNs to send the push message at a time that takes 27 | // into account power considerations for the device. Notifications with this 28 | // priority might be grouped and delivered in bursts. They are throttled, and 29 | // in some cases are not delivered. 30 | ApnsPriorityLow = 5 31 | 32 | // ApnsPriorityHigh will tell APNs to send the push message immediately. 33 | // Notifications with this priority must trigger an alert, sound, or badge on 34 | // the target device. It is an error to use this priority for a push 35 | // notification that contains only the content-available key. 36 | ApnsPriorityHigh = 10 37 | ) 38 | 39 | // Alert is APNs payload 40 | type Alert struct { 41 | Action string `json:"action,omitempty"` 42 | ActionLocKey string `json:"action-loc-key,omitempty"` 43 | Body string `json:"body,omitempty"` 44 | LaunchImage string `json:"launch-image,omitempty"` 45 | LocArgs []string `json:"loc-args,omitempty"` 46 | LocKey string `json:"loc-key,omitempty"` 47 | Title string `json:"title,omitempty"` 48 | Subtitle string `json:"subtitle,omitempty"` 49 | TitleLocArgs []string `json:"title-loc-args,omitempty"` 50 | TitleLocKey string `json:"title-loc-key,omitempty"` 51 | SummaryArg string `json:"summary-arg,omitempty"` 52 | SummaryArgCount int `json:"summary-arg-count,omitempty"` 53 | } 54 | 55 | // RequestPush support multiple notification request. 56 | type RequestPush struct { 57 | Notifications []PushNotification `json:"notifications" binding:"required"` 58 | } 59 | 60 | // ResponsePush response of notification request. 61 | type ResponsePush struct { 62 | Logs []logx.LogPushEntry `json:"logs"` 63 | } 64 | 65 | // PushNotification is single notification request 66 | type PushNotification struct { 67 | // Common 68 | ID string `json:"notif_id,omitempty"` 69 | To string `json:"to,omitempty"` 70 | Topic string `json:"topic,omitempty"` // FCM and iOS only 71 | Tokens []string `json:"tokens" binding:"required"` 72 | Platform int `json:"platform" binding:"required"` 73 | Message string `json:"message,omitempty"` 74 | Title string `json:"title,omitempty"` 75 | Image string `json:"image,omitempty"` 76 | Priority string `json:"priority,omitempty"` 77 | ContentAvailable bool `json:"content_available,omitempty"` 78 | MutableContent bool `json:"mutable_content,omitempty"` 79 | Sound interface{} `json:"sound,omitempty"` 80 | Data D `json:"data,omitempty"` 81 | Retry int `json:"retry,omitempty"` 82 | 83 | // Android 84 | Notification *messaging.Notification `json:"notification,omitempty"` 85 | Android *messaging.AndroidConfig `json:"android,omitempty"` 86 | Webpush *messaging.WebpushConfig `json:"webpush,omitempty"` 87 | APNS *messaging.APNSConfig `json:"apns,omitempty"` 88 | FCMOptions *messaging.FCMOptions `json:"fcm_options,omitempty"` 89 | Condition string `json:"condition,omitempty"` 90 | 91 | // Huawei 92 | AppID string `json:"app_id,omitempty"` 93 | AppSecret string `json:"app_secret,omitempty"` 94 | HuaweiNotification *model.AndroidNotification `json:"huawei_notification,omitempty"` 95 | HuaweiData string `json:"huawei_data,omitempty"` 96 | HuaweiCollapseKey int `json:"huawei_collapse_key,omitempty"` 97 | HuaweiTTL string `json:"huawei_ttl,omitempty"` 98 | BiTag string `json:"bi_tag,omitempty"` 99 | FastAppTarget int `json:"fast_app_target,omitempty"` 100 | 101 | // iOS 102 | Expiration *int64 `json:"expiration,omitempty"` 103 | ApnsID string `json:"apns_id,omitempty"` 104 | CollapseID string `json:"collapse_id,omitempty"` 105 | PushType string `json:"push_type,omitempty"` 106 | Badge *int `json:"badge,omitempty"` 107 | Category string `json:"category,omitempty"` 108 | ThreadID string `json:"thread-id,omitempty"` 109 | URLArgs []string `json:"url-args,omitempty"` 110 | Alert Alert `json:"alert,omitempty"` 111 | Production bool `json:"production,omitempty"` 112 | Development bool `json:"development,omitempty"` 113 | SoundName string `json:"name,omitempty"` 114 | SoundVolume float32 `json:"volume,omitempty"` 115 | 116 | // ref: https://github.com/sideshow/apns2/blob/54928d6193dfe300b6b88dad72b7e2ae138d4f0a/payload/builder.go#L7-L24 117 | InterruptionLevel string `json:"interruption_level,omitempty"` 118 | 119 | // live-activity support 120 | // ref: https://apple.co/3MLe2DB 121 | ContentState D `json:"content-state,omitempty"` 122 | StaleDate int64 `json:"stale-date,omitempty"` 123 | DismissalDate int64 `json:"dismissal-date"` 124 | Event string `json:"event,omitempty"` 125 | Timestamp int64 `json:"timestamp,omitempty"` 126 | } 127 | 128 | // Bytes for queue message 129 | func (p *PushNotification) Bytes() []byte { 130 | b, err := json.Marshal(p) 131 | if err != nil { 132 | panic(err) 133 | } 134 | return b 135 | } 136 | 137 | // Payload for queue message 138 | func (p *PushNotification) Payload() []byte { 139 | return nil 140 | } 141 | 142 | // IsTopic check if message format is topic for FCM 143 | // ref: https://firebase.google.com/docs/cloud-messaging/send-message#topic-http-post-request 144 | func (p *PushNotification) IsTopic() bool { 145 | if p.Platform == core.PlatFormHuawei || p.Platform == core.PlatFormAndroid { 146 | return p.Topic != "" || p.Condition != "" 147 | } 148 | 149 | return false 150 | } 151 | 152 | // CheckMessage for check request message 153 | func CheckMessage(req *PushNotification) error { 154 | var msg string 155 | 156 | if req.To != "" { 157 | req.Tokens = append(req.Tokens, req.To) 158 | } 159 | 160 | // if the message is a topic, the tokens field is not required 161 | if !req.IsTopic() && len(req.Tokens) == 0 { 162 | return errors.New("please provide at least one device token") 163 | } 164 | 165 | switch req.Platform { 166 | case core.PlatFormIos: 167 | if len(req.Tokens) == 1 && req.Tokens[0] == "" { 168 | msg = "the device token cannot be empty" 169 | logx.LogAccess.Debug(msg) 170 | return errors.New(msg) 171 | } 172 | case 173 | core.PlatFormAndroid, 174 | core.PlatFormHuawei: 175 | if len(req.Tokens) > 500 { 176 | // https://firebase.google.com/docs/cloud-messaging/send-message#send-messages-to-multiple-devices 177 | msg = "you can specify up to 500 device registration tokens per invocation" 178 | logx.LogAccess.Debug(msg) 179 | return errors.New(msg) 180 | } 181 | default: 182 | } 183 | 184 | return nil 185 | } 186 | 187 | // SetProxy only working for FCM server. 188 | func SetProxy(proxy string) error { 189 | proxyURL, err := url.ParseRequestURI(proxy) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | http.DefaultTransport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} 195 | logx.LogAccess.Debug("Set http proxy as " + proxy) 196 | 197 | return nil 198 | } 199 | 200 | // CheckPushConf provide check your yml config. 201 | func CheckPushConf(cfg *config.ConfYaml) error { 202 | if !cfg.Ios.Enabled && !cfg.Android.Enabled && !cfg.Huawei.Enabled { 203 | return errors.New("please enable iOS, Android or Huawei config in yml config") 204 | } 205 | 206 | if cfg.Ios.Enabled { 207 | if cfg.Ios.KeyPath == "" && cfg.Ios.KeyBase64 == "" { 208 | return errors.New("missing iOS certificate key") 209 | } 210 | 211 | // check certificate file exist 212 | if cfg.Ios.KeyPath != "" { 213 | if _, err := os.Stat(cfg.Ios.KeyPath); os.IsNotExist(err) { 214 | return errors.New("certificate file does not exist") 215 | } 216 | } 217 | } 218 | 219 | if cfg.Android.Enabled { 220 | credential := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") 221 | if cfg.Android.Credential == "" && 222 | cfg.Android.KeyPath == "" && 223 | credential == "" { 224 | return errors.New("missing fcm credential data") 225 | } 226 | } 227 | 228 | if cfg.Huawei.Enabled { 229 | if cfg.Huawei.AppSecret == "" { 230 | return errors.New("missing huawei app secret") 231 | } 232 | 233 | if cfg.Huawei.AppID == "" { 234 | return errors.New("missing huawei app id") 235 | } 236 | } 237 | 238 | return nil 239 | } 240 | 241 | // SendNotification provide send notification. 242 | func SendNotification( 243 | ctx context.Context, 244 | req qcore.TaskMessage, 245 | cfg *config.ConfYaml, 246 | ) (resp *ResponsePush, err error) { 247 | v, ok := req.(*PushNotification) 248 | if !ok { 249 | if err = json.Unmarshal(req.Payload(), &v); err != nil { 250 | return nil, err 251 | } 252 | } 253 | 254 | switch v.Platform { 255 | case core.PlatFormIos: 256 | resp, err = PushToIOS(ctx, v, cfg) 257 | case core.PlatFormAndroid: 258 | resp, err = PushToAndroid(ctx, v, cfg) 259 | case core.PlatFormHuawei: 260 | resp, err = PushToHuawei(ctx, v, cfg) 261 | } 262 | 263 | if cfg.Core.FeedbackURL != "" { 264 | var logs []logx.LogPushEntry 265 | 266 | if resp != nil { 267 | logs = resp.Logs 268 | } else { 269 | logs = makeErrorLogs(cfg, v, err) 270 | } 271 | 272 | for _, l := range logs { 273 | err := DispatchFeedback(ctx, l, cfg.Core.FeedbackURL, cfg.Core.FeedbackTimeout, cfg.Core.FeedbackHeader) 274 | if err != nil { 275 | logx.LogError.Error(err) 276 | } 277 | } 278 | } 279 | 280 | return resp, err 281 | } 282 | 283 | // makeErrorLogs creates a list of LogPushEntries for each token in notification 284 | // in case when logs are not returned from PushToXYZ() and error err is not nil 285 | func makeErrorLogs( 286 | cfg *config.ConfYaml, 287 | notification *PushNotification, 288 | err error, 289 | ) []logx.LogPushEntry { 290 | if err == nil { 291 | return []logx.LogPushEntry{} 292 | } 293 | 294 | logs := make([]logx.LogPushEntry, 0, len(notification.Tokens)) 295 | 296 | for _, token := range notification.Tokens { 297 | log := logPush(cfg, core.FailedPush, token, notification, err) 298 | 299 | logs = append(logs, log) 300 | } 301 | 302 | return logs 303 | } 304 | 305 | // Run send notification 306 | var Run = func(cfg *config.ConfYaml) func(ctx context.Context, msg qcore.TaskMessage) error { 307 | return func(ctx context.Context, msg qcore.TaskMessage) error { 308 | _, err := SendNotification(ctx, msg, cfg) 309 | return err 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /notify/notification_fcm.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/appleboy/gorush/config" 10 | "github.com/appleboy/gorush/core" 11 | "github.com/appleboy/gorush/logx" 12 | "github.com/appleboy/gorush/status" 13 | 14 | "firebase.google.com/go/v4/messaging" 15 | "github.com/appleboy/go-fcm" 16 | ) 17 | 18 | func fileExists(filename string) bool { 19 | info, err := os.Stat(filename) 20 | if os.IsNotExist(err) { 21 | return false 22 | } 23 | return !info.IsDir() 24 | } 25 | 26 | // InitFCMClient use for initialize FCM Client. 27 | func InitFCMClient(ctx context.Context, cfg *config.ConfYaml) (*fcm.Client, error) { 28 | var opts []fcm.Option 29 | 30 | credential := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") 31 | if cfg.Android.Credential == "" && 32 | cfg.Android.KeyPath == "" && 33 | credential == "" { 34 | return nil, errors.New("missing fcm credential data") 35 | } 36 | 37 | if cfg.Android.KeyPath != "" && fileExists(cfg.Android.KeyPath) { 38 | opts = append(opts, fcm.WithCredentialsFile(cfg.Android.KeyPath)) 39 | } 40 | 41 | if cfg.Android.Credential != "" { 42 | opts = append(opts, fcm.WithCredentialsJSON([]byte(cfg.Android.Credential))) 43 | } 44 | 45 | if FCMClient != nil { 46 | return FCMClient, nil 47 | } 48 | 49 | var err error 50 | FCMClient, err = fcm.NewClient( 51 | ctx, 52 | opts..., 53 | ) 54 | 55 | return FCMClient, err 56 | } 57 | 58 | // GetAndroidNotification use for define Android notification. 59 | // HTTP Connection Server Reference for Android 60 | // https://firebase.google.com/docs/cloud-messaging/http-server-ref 61 | func GetAndroidNotification(req *PushNotification) []*messaging.Message { 62 | var messages []*messaging.Message 63 | 64 | if req.Title != "" || req.Message != "" || req.Image != "" { 65 | if req.Notification == nil { 66 | req.Notification = &messaging.Notification{} 67 | } 68 | if req.Title != "" { 69 | req.Notification.Title = req.Title 70 | } 71 | if req.Message != "" { 72 | req.Notification.Body = req.Message 73 | } 74 | if req.Image != "" { 75 | req.Notification.ImageURL = req.Image 76 | } 77 | if req.MutableContent { 78 | req.APNS = &messaging.APNSConfig{ 79 | Payload: &messaging.APNSPayload{ 80 | Aps: &messaging.Aps{ 81 | MutableContent: req.MutableContent, 82 | }, 83 | }, 84 | } 85 | } 86 | } 87 | 88 | // content-available is for background notifications and a badge, alert 89 | // and sound keys should not be present. 90 | // See: https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification 91 | if req.ContentAvailable { 92 | req.APNS = &messaging.APNSConfig{ 93 | Headers: map[string]string{ 94 | "apns-priority": "5", 95 | }, 96 | Payload: &messaging.APNSPayload{ 97 | Aps: &messaging.Aps{ 98 | ContentAvailable: req.ContentAvailable, 99 | CustomData: req.Data, 100 | }, 101 | }, 102 | } 103 | } 104 | 105 | // Check if the notification has a sound 106 | if req.Sound != nil { 107 | sound, ok := req.Sound.(string) 108 | if ok { 109 | switch { 110 | case req.APNS == nil: 111 | req.APNS = &messaging.APNSConfig{ 112 | Payload: &messaging.APNSPayload{ 113 | Aps: &messaging.Aps{ 114 | Sound: sound, 115 | }, 116 | }, 117 | } 118 | case req.APNS.Payload == nil: 119 | req.APNS.Payload = &messaging.APNSPayload{ 120 | Aps: &messaging.Aps{ 121 | Sound: sound, 122 | }, 123 | } 124 | 125 | case req.APNS.Payload.Aps == nil: 126 | req.APNS.Payload.Aps = &messaging.Aps{ 127 | Sound: sound, 128 | } 129 | default: 130 | req.APNS.Payload.Aps.Sound = sound 131 | } 132 | 133 | if req.Android == nil { 134 | req.Android = &messaging.AndroidConfig{ 135 | Priority: req.Priority, 136 | Notification: &messaging.AndroidNotification{ 137 | Sound: sound, 138 | }, 139 | } 140 | } 141 | } 142 | } 143 | 144 | var data map[string]string 145 | if len(req.Data) > 0 { 146 | data = make(map[string]string, len(req.Data)) 147 | for k, v := range req.Data { 148 | switch v.(type) { 149 | case string: 150 | data[k] = fmt.Sprintf("%s", v) 151 | default: 152 | if v, err := json.Marshal(v); err == nil { 153 | data[k] = string(v) 154 | } 155 | } 156 | } 157 | } 158 | 159 | // Check if the notification is a topic 160 | if req.IsTopic() { 161 | message := &messaging.Message{ 162 | Notification: req.Notification, 163 | Android: req.Android, 164 | Webpush: req.Webpush, 165 | APNS: req.APNS, 166 | FCMOptions: req.FCMOptions, 167 | Topic: req.Topic, 168 | Condition: req.Condition, 169 | } 170 | 171 | // Add another field 172 | if len(req.Data) > 0 { 173 | message.Data = data 174 | } 175 | 176 | messages = append(messages, message) 177 | } 178 | 179 | // Loop through the tokens and create a message for each one 180 | for _, token := range req.Tokens { 181 | message := &messaging.Message{ 182 | Token: token, 183 | Notification: req.Notification, 184 | Android: req.Android, 185 | Webpush: req.Webpush, 186 | APNS: req.APNS, 187 | FCMOptions: req.FCMOptions, 188 | } 189 | 190 | // Add another field 191 | if len(req.Data) > 0 { 192 | message.Data = data 193 | } 194 | 195 | messages = append(messages, message) 196 | } 197 | 198 | return messages 199 | } 200 | 201 | // PushToAndroid provide send notification to Android server. 202 | func PushToAndroid(ctx context.Context, req *PushNotification, cfg *config.ConfYaml) (resp *ResponsePush, err error) { 203 | logx.LogAccess.Debug("Start push notification for Android") 204 | 205 | var ( 206 | client *fcm.Client 207 | retryCount = 0 208 | maxRetry = cfg.Android.MaxRetry 209 | ) 210 | 211 | if req.Retry > 0 && req.Retry < maxRetry { 212 | maxRetry = req.Retry 213 | } 214 | 215 | // check message 216 | err = CheckMessage(req) 217 | if err != nil { 218 | logx.LogError.Error("request error: " + err.Error()) 219 | return nil, err 220 | } 221 | 222 | resp = &ResponsePush{} 223 | client, err = InitFCMClient(ctx, cfg) 224 | 225 | Retry: 226 | messages := GetAndroidNotification(req) 227 | if err != nil { 228 | // FCM server error 229 | logx.LogError.Error("FCM server error: " + err.Error()) 230 | return resp, err 231 | } 232 | 233 | if req.Development { 234 | for i, msg := range messages { 235 | m, _ := json.Marshal(msg) 236 | logx.LogAccess.Infof("message #%d - %s", i, m) 237 | } 238 | } 239 | 240 | res, err := client.Send(ctx, messages...) 241 | if err != nil { 242 | newErr := fmt.Errorf("fcm service send message error: %v", err) 243 | logx.LogError.Error(newErr) 244 | errLog := logPush(cfg, core.FailedPush, "", req, newErr) 245 | resp.Logs = append(resp.Logs, errLog) 246 | status.StatStorage.AddAndroidError(1) 247 | 248 | return resp, newErr 249 | } 250 | 251 | logx.LogAccess.Debug(fmt.Sprintf("Android Success count: %d, Failure count: %d", res.SuccessCount, res.FailureCount)) 252 | status.StatStorage.AddAndroidSuccess(int64(res.SuccessCount)) 253 | status.StatStorage.AddAndroidError(int64(res.FailureCount)) 254 | 255 | // result from Send messages to topics 256 | retryTopic := false 257 | if req.IsTopic() { 258 | to := "" 259 | if req.Topic != "" { 260 | to = req.Topic 261 | } 262 | if req.Condition != "" { 263 | to = req.Condition 264 | } 265 | logx.LogAccess.Debug("Send Topic Message: ", to) 266 | 267 | newResp := res.Responses[0] 268 | if newResp.Success { 269 | logPush(cfg, core.SucceededPush, to, req, nil) 270 | } 271 | 272 | if newResp.Error != nil { 273 | // failure 274 | errLog := logPush(cfg, core.FailedPush, to, req, newResp.Error) 275 | resp.Logs = append(resp.Logs, errLog) 276 | retryTopic = true 277 | } 278 | 279 | // remove the first response 280 | res.Responses = res.Responses[1:] 281 | } 282 | 283 | var newTokens []string 284 | for k, result := range res.Responses { 285 | if result.Error != nil { 286 | errLog := logPush(cfg, core.FailedPush, req.Tokens[k], req, result.Error) 287 | resp.Logs = append(resp.Logs, errLog) 288 | newTokens = append(newTokens, req.Tokens[k]) 289 | continue 290 | } 291 | logPush(cfg, core.SucceededPush, req.Tokens[k], req, nil) 292 | } 293 | 294 | if len(newTokens) > 0 && retryCount < maxRetry { 295 | retryCount++ 296 | 297 | if req.IsTopic() && !retryTopic { 298 | req.Topic = "" 299 | req.Condition = "" 300 | } 301 | 302 | // resend fail token 303 | req.Tokens = newTokens 304 | goto Retry 305 | } 306 | 307 | return resp, nil 308 | } 309 | 310 | func logPush(cfg *config.ConfYaml, status, token string, req *PushNotification, err error) logx.LogPushEntry { 311 | return logx.LogPush(&logx.InputLog{ 312 | ID: req.ID, 313 | Status: status, 314 | Token: token, 315 | Message: req.Message, 316 | Platform: req.Platform, 317 | Error: err, 318 | HideToken: cfg.Log.HideToken, 319 | HideMessage: cfg.Log.HideMessages, 320 | Format: cfg.Log.Format, 321 | }) 322 | } 323 | -------------------------------------------------------------------------------- /notify/notification_fcm_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | "firebase.google.com/go/v4/messaging" 10 | "github.com/appleboy/gorush/config" 11 | "github.com/appleboy/gorush/core" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestMissingAndroidCredential(t *testing.T) { 17 | cfg, _ := config.LoadConf() 18 | 19 | cfg.Android.Enabled = true 20 | cfg.Android.Credential = "" 21 | 22 | err := CheckPushConf(cfg) 23 | 24 | assert.Error(t, err) 25 | assert.Equal(t, "missing fcm credential data", err.Error()) 26 | } 27 | 28 | func TestMissingKeyForInitFCMClient(t *testing.T) { 29 | cfg, _ := config.LoadConf() 30 | cfg.Android.Credential = "" 31 | cfg.Android.KeyPath = "" 32 | client, err := InitFCMClient(context.Background(), cfg) 33 | 34 | assert.Nil(t, client) 35 | assert.Error(t, err) 36 | assert.Equal(t, "missing fcm credential data", err.Error()) 37 | } 38 | 39 | func TestPushToAndroidWrongToken(t *testing.T) { 40 | cfg, _ := config.LoadConf() 41 | 42 | cfg.Android.Enabled = true 43 | cfg.Android.Credential = os.Getenv("FCM_CREDENTIAL") 44 | 45 | req := &PushNotification{ 46 | Tokens: []string{"aaaaaa", "bbbbb"}, 47 | Platform: core.PlatFormAndroid, 48 | Message: "Welcome", 49 | } 50 | 51 | // Android Success count: 0, Failure count: 2 52 | resp, err := PushToAndroid(context.Background(), req, cfg) 53 | assert.Nil(t, err) 54 | assert.Len(t, resp.Logs, 2) 55 | } 56 | 57 | func TestPushToAndroidRightTokenForJSONLog(t *testing.T) { 58 | cfg, _ := config.LoadConf() 59 | 60 | cfg.Android.Enabled = true 61 | cfg.Android.Credential = os.Getenv("FCM_CREDENTIAL") 62 | // log for json 63 | cfg.Log.Format = "json" 64 | 65 | androidToken := os.Getenv("FCM_TEST_TOKEN") 66 | 67 | req := &PushNotification{ 68 | Tokens: []string{androidToken}, 69 | Platform: core.PlatFormAndroid, 70 | Message: "Welcome", 71 | } 72 | 73 | resp, err := PushToAndroid(context.Background(), req, cfg) 74 | assert.Nil(t, err) 75 | assert.Len(t, resp.Logs, 0) 76 | } 77 | 78 | func TestPushToAndroidRightTokenForStringLog(t *testing.T) { 79 | cfg, _ := config.LoadConf() 80 | 81 | cfg.Android.Enabled = true 82 | cfg.Android.Credential = os.Getenv("FCM_CREDENTIAL") 83 | 84 | androidToken := os.Getenv("FCM_TEST_TOKEN") 85 | 86 | req := &PushNotification{ 87 | Tokens: []string{androidToken}, 88 | Platform: core.PlatFormAndroid, 89 | Message: "Welcome", 90 | } 91 | 92 | resp, err := PushToAndroid(context.Background(), req, cfg) 93 | assert.Nil(t, err) 94 | assert.Len(t, resp.Logs, 0) 95 | } 96 | 97 | func TestFCMMessage(t *testing.T) { 98 | var err error 99 | 100 | // the message must specify at least one registration ID 101 | req := &PushNotification{ 102 | Message: "Test", 103 | Tokens: []string{}, 104 | } 105 | 106 | err = CheckMessage(req) 107 | assert.Error(t, err) 108 | 109 | // ignore check token length if send topic message 110 | req = &PushNotification{ 111 | Message: "Test", 112 | Platform: core.PlatFormAndroid, 113 | Topic: "/topics/foo-bar", 114 | } 115 | 116 | err = CheckMessage(req) 117 | assert.NoError(t, err) 118 | 119 | // "condition": "'dogs' in topics || 'cats' in topics", 120 | req = &PushNotification{ 121 | Message: "Test", 122 | Platform: core.PlatFormAndroid, 123 | Condition: "'dogs' in topics || 'cats' in topics", 124 | } 125 | 126 | err = CheckMessage(req) 127 | assert.NoError(t, err) 128 | 129 | // the message may specify at most 1000 registration IDs 130 | req = &PushNotification{ 131 | Message: "Test", 132 | Platform: core.PlatFormAndroid, 133 | Tokens: make([]string, 501), 134 | } 135 | 136 | err = CheckMessage(req) 137 | assert.Error(t, err) 138 | 139 | // Pass 140 | req = &PushNotification{ 141 | Message: "Test", 142 | Platform: core.PlatFormAndroid, 143 | Tokens: []string{"XXXXXXXXX"}, 144 | } 145 | 146 | err = CheckMessage(req) 147 | assert.NoError(t, err) 148 | } 149 | 150 | func TestAndroidNotificationStructure(t *testing.T) { 151 | test := "test" 152 | req := &PushNotification{ 153 | Tokens: []string{"a", "b"}, 154 | Message: "Welcome", 155 | To: test, 156 | Priority: HIGH, 157 | MutableContent: true, 158 | Title: test, 159 | Sound: test, 160 | Data: D{ 161 | "a": "1", 162 | "b": 2, 163 | "json": map[string]interface{}{ 164 | "c": "3", 165 | "d": 4, 166 | }, 167 | }, 168 | Notification: &messaging.Notification{ 169 | Title: test, 170 | Body: "", 171 | }, 172 | } 173 | 174 | messages := GetAndroidNotification(req) 175 | 176 | assert.Equal(t, test, messages[0].Notification.Title) 177 | assert.Equal(t, "Welcome", messages[0].Notification.Body) 178 | assert.Equal(t, "1", messages[0].Data["a"]) 179 | assert.Equal(t, "2", messages[0].Data["b"]) 180 | assert.Equal(t, "{\"c\":\"3\",\"d\":4}", messages[0].Data["json"]) 181 | assert.NotNil(t, messages[0].APNS) 182 | assert.Equal(t, req.Sound, messages[0].APNS.Payload.Aps.Sound) 183 | assert.Equal(t, req.MutableContent, messages[0].APNS.Payload.Aps.MutableContent) 184 | 185 | // test empty body 186 | req = &PushNotification{ 187 | Tokens: []string{"a", "b"}, 188 | To: test, 189 | Notification: &messaging.Notification{ 190 | Body: "", 191 | }, 192 | } 193 | messages = GetAndroidNotification(req) 194 | 195 | assert.Equal(t, "", messages[0].Notification.Body) 196 | } 197 | 198 | func TestAndroidBackgroundNotificationStructure(t *testing.T) { 199 | data := map[string]any{ 200 | "a": "1", 201 | "b": 2, 202 | "json": map[string]interface{}{ 203 | "c": "3", 204 | "d": 4, 205 | }, 206 | } 207 | req := &PushNotification{ 208 | Tokens: []string{"a", "b"}, 209 | Priority: HIGH, 210 | ContentAvailable: true, 211 | Data: data, 212 | } 213 | 214 | messages := GetAndroidNotification(req) 215 | 216 | assert.Equal(t, "1", messages[0].Data["a"]) 217 | assert.Equal(t, "2", messages[0].Data["b"]) 218 | assert.Equal(t, "{\"c\":\"3\",\"d\":4}", messages[0].Data["json"]) 219 | assert.NotNil(t, messages[0].APNS) 220 | assert.Equal(t, req.ContentAvailable, messages[0].APNS.Payload.Aps.ContentAvailable) 221 | assert.True(t, reflect.DeepEqual(data, messages[0].APNS.Payload.Aps.CustomData)) 222 | } 223 | -------------------------------------------------------------------------------- /notify/notification_hms.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | 8 | "github.com/appleboy/gorush/config" 9 | "github.com/appleboy/gorush/core" 10 | "github.com/appleboy/gorush/logx" 11 | "github.com/appleboy/gorush/status" 12 | 13 | c "github.com/appleboy/go-hms-push/push/config" 14 | client "github.com/appleboy/go-hms-push/push/core" 15 | "github.com/appleboy/go-hms-push/push/model" 16 | ) 17 | 18 | var ( 19 | pushError error 20 | pushClient *client.HMSClient 21 | once sync.Once 22 | ) 23 | 24 | // GetPushClient use for create HMS Push. 25 | func GetPushClient(conf *c.Config) (*client.HMSClient, error) { 26 | once.Do(func() { 27 | client, err := client.NewHttpClient(conf) 28 | if err != nil { 29 | panic(err) 30 | } 31 | pushClient = client 32 | pushError = err 33 | }) 34 | 35 | return pushClient, pushError 36 | } 37 | 38 | // InitHMSClient use for initialize HMS Client. 39 | func InitHMSClient(cfg *config.ConfYaml, appSecret, appID string) (*client.HMSClient, error) { 40 | if appSecret == "" { 41 | return nil, errors.New("missing huawei app secret") 42 | } 43 | 44 | if appID == "" { 45 | return nil, errors.New("missing huawei app id") 46 | } 47 | 48 | conf := &c.Config{ 49 | AppId: appID, 50 | AppSecret: appSecret, 51 | AuthUrl: "https://oauth-login.cloud.huawei.com/oauth2/v3/token", 52 | PushUrl: "https://push-api.cloud.huawei.com", 53 | } 54 | 55 | if appSecret != cfg.Huawei.AppSecret || appID != cfg.Huawei.AppID { 56 | return GetPushClient(conf) 57 | } 58 | 59 | if HMSClient == nil { 60 | return GetPushClient(conf) 61 | } 62 | 63 | return HMSClient, nil 64 | } 65 | 66 | // GetHuaweiNotification use for define HMS notification. 67 | // HTTP Connection Server Reference for HMS 68 | // https://developer.huawei.com/consumer/en/doc/development/HMS-References/push-sendapi 69 | func GetHuaweiNotification(req *PushNotification) (*model.MessageRequest, error) { 70 | msgRequest := model.NewNotificationMsgRequest() 71 | 72 | msgRequest.Message.Android = model.GetDefaultAndroid() 73 | 74 | if len(req.Tokens) > 0 { 75 | msgRequest.Message.Token = req.Tokens 76 | } 77 | 78 | if len(req.Topic) > 0 { 79 | msgRequest.Message.Topic = req.Topic 80 | } 81 | 82 | if len(req.Condition) > 0 { 83 | msgRequest.Message.Condition = req.Condition 84 | } 85 | 86 | if req.Priority == HIGH { 87 | msgRequest.Message.Android.Urgency = "HIGH" 88 | } 89 | 90 | // if req.HuaweiCollapseKey != nil { 91 | msgRequest.Message.Android.CollapseKey = req.HuaweiCollapseKey 92 | //} 93 | 94 | if len(req.Category) > 0 { 95 | msgRequest.Message.Android.Category = req.Category 96 | } 97 | 98 | if len(req.HuaweiTTL) > 0 { 99 | msgRequest.Message.Android.TTL = req.HuaweiTTL 100 | } 101 | 102 | if len(req.BiTag) > 0 { 103 | msgRequest.Message.Android.BiTag = req.BiTag 104 | } 105 | 106 | msgRequest.Message.Android.FastAppTarget = req.FastAppTarget 107 | 108 | // Add data fields 109 | if len(req.HuaweiData) > 0 { 110 | msgRequest.Message.Data = req.HuaweiData 111 | } 112 | 113 | // Notification Message 114 | if req.HuaweiNotification != nil { 115 | msgRequest.Message.Android.Notification = req.HuaweiNotification 116 | 117 | if msgRequest.Message.Android.Notification.ClickAction == nil { 118 | msgRequest.Message.Android.Notification.ClickAction = model.GetDefaultClickAction() 119 | } 120 | } 121 | 122 | setDefaultAndroidNotification := func() { 123 | if msgRequest.Message.Android.Notification == nil { 124 | msgRequest.Message.Android.Notification = model.GetDefaultAndroidNotification() 125 | } 126 | } 127 | 128 | if len(req.Message) > 0 { 129 | setDefaultAndroidNotification() 130 | msgRequest.Message.Android.Notification.Body = req.Message 131 | } 132 | 133 | if len(req.Title) > 0 { 134 | setDefaultAndroidNotification() 135 | msgRequest.Message.Android.Notification.Title = req.Title 136 | } 137 | 138 | if len(req.Image) > 0 { 139 | setDefaultAndroidNotification() 140 | msgRequest.Message.Android.Notification.Image = req.Image 141 | } 142 | 143 | if v, ok := req.Sound.(string); ok && len(v) > 0 { 144 | setDefaultAndroidNotification() 145 | msgRequest.Message.Android.Notification.Sound = v 146 | } else if msgRequest.Message.Android.Notification != nil { 147 | msgRequest.Message.Android.Notification.DefaultSound = true 148 | } 149 | 150 | b, err := json.Marshal(msgRequest) 151 | if err != nil { 152 | logx.LogError.Error("Failed to marshal the default message! Error is " + err.Error()) 153 | return nil, err 154 | } 155 | 156 | logx.LogAccess.Debugf("Default message is %s", string(b)) 157 | return msgRequest, nil 158 | } 159 | 160 | // PushToHuawei provide send notification to Android server. 161 | func PushToHuawei(ctx context.Context, req *PushNotification, cfg *config.ConfYaml) (resp *ResponsePush, err error) { 162 | logx.LogAccess.Debug("Start push notification for Huawei") 163 | 164 | var ( 165 | client *client.HMSClient 166 | retryCount = 0 167 | maxRetry = cfg.Huawei.MaxRetry 168 | ) 169 | 170 | if req.Retry > 0 && req.Retry < maxRetry { 171 | maxRetry = req.Retry 172 | } 173 | 174 | // check message 175 | err = CheckMessage(req) 176 | if err != nil { 177 | logx.LogError.Error("request error: " + err.Error()) 178 | return nil, err 179 | } 180 | 181 | client, err = InitHMSClient(cfg, cfg.Huawei.AppSecret, cfg.Huawei.AppID) 182 | if err != nil { 183 | // HMS server error 184 | logx.LogError.Error("HMS server error: " + err.Error()) 185 | return nil, err 186 | } 187 | 188 | resp = &ResponsePush{} 189 | 190 | Retry: 191 | isError := false 192 | 193 | notification, _ := GetHuaweiNotification(req) 194 | 195 | res, err := client.SendMessage(ctx, notification) 196 | if err != nil { 197 | // Send Message error 198 | errLog := logPush(cfg, core.FailedPush, req.Topic, req, err) 199 | resp.Logs = append(resp.Logs, errLog) 200 | logx.LogError.Error("HMS server send message error: " + err.Error()) 201 | return resp, err 202 | } 203 | 204 | // Huawei Push Send API does not support exact results for each token 205 | if res.Code == "80000000" { 206 | status.StatStorage.AddHuaweiSuccess(int64(1)) 207 | logx.LogAccess.Debug("Huwaei Send Notification is completed successfully!") 208 | } else { 209 | isError = true 210 | status.StatStorage.AddHuaweiError(int64(1)) 211 | logx.LogAccess.Debug("Huawei Send Notification is failed! Code: " + res.Code) 212 | } 213 | 214 | if isError && retryCount < maxRetry { 215 | retryCount++ 216 | 217 | // resend all tokens 218 | goto Retry 219 | } 220 | 221 | return resp, nil 222 | } 223 | -------------------------------------------------------------------------------- /notify/notification_hms_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/appleboy/gorush/config" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMissingHuaweiAppSecret(t *testing.T) { 12 | cfg, _ := config.LoadConf() 13 | 14 | cfg.Android.Enabled = false 15 | cfg.Huawei.Enabled = true 16 | cfg.Huawei.AppSecret = "" 17 | 18 | err := CheckPushConf(cfg) 19 | 20 | assert.Error(t, err) 21 | assert.Equal(t, "missing huawei app secret", err.Error()) 22 | } 23 | 24 | func TestMissingHuaweiAppID(t *testing.T) { 25 | cfg, _ := config.LoadConf() 26 | 27 | cfg.Android.Enabled = false 28 | cfg.Huawei.Enabled = true 29 | cfg.Huawei.AppID = "" 30 | 31 | err := CheckPushConf(cfg) 32 | 33 | assert.Error(t, err) 34 | assert.Equal(t, "missing huawei app id", err.Error()) 35 | } 36 | 37 | func TestMissingAppSecretForInitHMSClient(t *testing.T) { 38 | cfg, _ := config.LoadConf() 39 | client, err := InitHMSClient(cfg, "", "APP_SECRET") 40 | 41 | assert.Nil(t, client) 42 | assert.Error(t, err) 43 | assert.Equal(t, "missing huawei app secret", err.Error()) 44 | } 45 | 46 | func TestMissingAppIDForInitHMSClient(t *testing.T) { 47 | cfg, _ := config.LoadConf() 48 | client, err := InitHMSClient(cfg, "APP_ID", "") 49 | 50 | assert.Nil(t, client) 51 | assert.Error(t, err) 52 | assert.Equal(t, "missing huawei app id", err.Error()) 53 | } 54 | -------------------------------------------------------------------------------- /notify/notification_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/appleboy/gorush/config" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCorrectConf(t *testing.T) { 12 | cfg, _ := config.LoadConf() 13 | 14 | cfg.Android.Enabled = true 15 | cfg.Android.Credential = "xxxxx" 16 | 17 | cfg.Ios.Enabled = true 18 | cfg.Ios.KeyPath = testKeyPath 19 | 20 | err := CheckPushConf(cfg) 21 | 22 | assert.NoError(t, err) 23 | } 24 | 25 | func TestSetProxyURL(t *testing.T) { 26 | err := SetProxy("87.236.233.92:8080") 27 | assert.Error(t, err) 28 | assert.Equal(t, "parse \"87.236.233.92:8080\": invalid URI for request", err.Error()) 29 | 30 | err = SetProxy("a.html") 31 | assert.Error(t, err) 32 | 33 | err = SetProxy("http://87.236.233.92:8080") 34 | assert.NoError(t, err) 35 | } 36 | -------------------------------------------------------------------------------- /router/server.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "sync" 11 | 12 | "github.com/appleboy/gorush/config" 13 | "github.com/appleboy/gorush/core" 14 | "github.com/appleboy/gorush/logx" 15 | "github.com/appleboy/gorush/metric" 16 | "github.com/appleboy/gorush/notify" 17 | "github.com/appleboy/gorush/status" 18 | 19 | api "github.com/appleboy/gin-status-api" 20 | "github.com/gin-contrib/logger" 21 | "github.com/gin-gonic/gin" 22 | "github.com/gin-gonic/gin/binding" 23 | "github.com/golang-queue/queue" 24 | "github.com/mattn/go-isatty" 25 | "github.com/prometheus/client_golang/prometheus" 26 | "github.com/prometheus/client_golang/prometheus/promhttp" 27 | "github.com/rs/zerolog" 28 | "github.com/rs/zerolog/log" 29 | "github.com/thoas/stats" 30 | "golang.org/x/crypto/acme/autocert" 31 | ) 32 | 33 | var doOnce sync.Once 34 | 35 | func abortWithError(c *gin.Context, code int, message string) { 36 | c.AbortWithStatusJSON(code, gin.H{ 37 | "code": code, 38 | "message": message, 39 | }) 40 | } 41 | 42 | func rootHandler(c *gin.Context) { 43 | c.JSON(http.StatusOK, gin.H{ 44 | "text": "Welcome to notification server.", 45 | }) 46 | } 47 | 48 | func heartbeatHandler(c *gin.Context) { 49 | c.AbortWithStatus(http.StatusOK) 50 | } 51 | 52 | func versionHandler(c *gin.Context) { 53 | c.JSON(http.StatusOK, gin.H{ 54 | "source": "https://github.com/appleboy/gorush", 55 | "version": GetVersion(), 56 | }) 57 | } 58 | 59 | func pushHandler(cfg *config.ConfYaml, q *queue.Queue) gin.HandlerFunc { 60 | return func(c *gin.Context) { 61 | var form notify.RequestPush 62 | var msg string 63 | 64 | if err := c.ShouldBindWith(&form, binding.JSON); err != nil { 65 | msg = "Missing notifications field." 66 | logx.LogAccess.Debug(err) 67 | abortWithError(c, http.StatusBadRequest, msg) 68 | return 69 | } 70 | 71 | if len(form.Notifications) == 0 { 72 | msg = "Notifications field is empty." 73 | logx.LogAccess.Debug(msg) 74 | abortWithError(c, http.StatusBadRequest, msg) 75 | return 76 | } 77 | 78 | if int64(len(form.Notifications)) > cfg.Core.MaxNotification { 79 | msg = fmt.Sprintf("Number of notifications(%d) over limit(%d)", len(form.Notifications), cfg.Core.MaxNotification) 80 | logx.LogAccess.Debug(msg) 81 | abortWithError(c, http.StatusBadRequest, msg) 82 | return 83 | } 84 | 85 | ctx, cancel := context.WithCancel(context.Background()) 86 | go func() { 87 | // Deprecated: the CloseNotifier interface predates Go's context package. 88 | // New code should use Request.Context instead. 89 | // Change to context package 90 | <-c.Request.Context().Done() 91 | // Don't send notification after client timeout or disconnected. 92 | // See the following issue for detail information. 93 | // https://github.com/appleboy/gorush/issues/422 94 | if cfg.Core.Sync { 95 | cancel() 96 | } 97 | }() 98 | 99 | counts, logs := handleNotification(ctx, cfg, form, q) 100 | 101 | c.JSON(http.StatusOK, gin.H{ 102 | "success": "ok", 103 | "counts": counts, 104 | "logs": logs, 105 | }) 106 | } 107 | } 108 | 109 | func configHandler(cfg *config.ConfYaml) gin.HandlerFunc { 110 | return func(c *gin.Context) { 111 | c.YAML(http.StatusCreated, cfg) 112 | } 113 | } 114 | 115 | func metricsHandler(c *gin.Context) { 116 | promhttp.Handler().ServeHTTP(c.Writer, c.Request) 117 | } 118 | 119 | func appStatusHandler(q *queue.Queue) gin.HandlerFunc { 120 | return func(c *gin.Context) { 121 | result := status.App{} 122 | 123 | result.Version = GetVersion() 124 | result.BusyWorkers = q.BusyWorkers() 125 | result.SuccessTasks = q.SuccessTasks() 126 | result.FailureTasks = q.FailureTasks() 127 | result.SubmittedTasks = q.SubmittedTasks() 128 | result.TotalCount = status.StatStorage.GetTotalCount() 129 | result.Ios.PushSuccess = status.StatStorage.GetIosSuccess() 130 | result.Ios.PushError = status.StatStorage.GetIosError() 131 | result.Android.PushSuccess = status.StatStorage.GetAndroidSuccess() 132 | result.Android.PushError = status.StatStorage.GetAndroidError() 133 | result.Huawei.PushSuccess = status.StatStorage.GetHuaweiSuccess() 134 | result.Huawei.PushError = status.StatStorage.GetHuaweiError() 135 | 136 | c.JSON(http.StatusOK, result) 137 | } 138 | } 139 | 140 | func sysStatsHandler() gin.HandlerFunc { 141 | return func(c *gin.Context) { 142 | c.JSON(http.StatusOK, status.Stats.Data()) 143 | } 144 | } 145 | 146 | // StatMiddleware response time, status code count, etc. 147 | func StatMiddleware() gin.HandlerFunc { 148 | return func(c *gin.Context) { 149 | beginning, recorder := status.Stats.Begin(c.Writer) 150 | c.Next() 151 | status.Stats.End(beginning, stats.WithRecorder(recorder)) 152 | } 153 | } 154 | 155 | func autoTLSServer(cfg *config.ConfYaml, q *queue.Queue) *http.Server { 156 | m := autocert.Manager{ 157 | Prompt: autocert.AcceptTOS, 158 | HostPolicy: autocert.HostWhitelist(cfg.Core.AutoTLS.Host), 159 | Cache: autocert.DirCache(cfg.Core.AutoTLS.Folder), 160 | } 161 | 162 | //nolint:gosec 163 | return &http.Server{ 164 | Addr: ":https", 165 | TLSConfig: &tls.Config{GetCertificate: m.GetCertificate}, 166 | Handler: routerEngine(cfg, q), 167 | } 168 | } 169 | 170 | func routerEngine(cfg *config.ConfYaml, q *queue.Queue) *gin.Engine { 171 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 172 | if cfg.Core.Mode == "debug" { 173 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 174 | } 175 | 176 | log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger() 177 | 178 | isTerm := isatty.IsTerminal(os.Stdout.Fd()) 179 | if isTerm { 180 | log.Logger = log.Output( 181 | zerolog.ConsoleWriter{ 182 | Out: os.Stdout, 183 | NoColor: false, 184 | }, 185 | ) 186 | } 187 | 188 | // Support metrics 189 | doOnce.Do(func() { 190 | m := metric.NewMetrics(q) 191 | prometheus.MustRegister(m) 192 | }) 193 | 194 | // set server mode 195 | gin.SetMode(cfg.Core.Mode) 196 | 197 | r := gin.New() 198 | 199 | // Global middleware 200 | r.Use(logger.SetLogger( 201 | logger.WithUTC(true), 202 | logger.WithSkipPath([]string{ 203 | cfg.API.HealthURI, 204 | cfg.API.MetricURI, 205 | }), 206 | )) 207 | r.Use(gin.Recovery()) 208 | r.Use(VersionMiddleware()) 209 | r.Use(StatMiddleware()) 210 | 211 | r.GET(cfg.API.StatGoURI, api.GinHandler) 212 | r.GET(cfg.API.StatAppURI, appStatusHandler(q)) 213 | r.GET(cfg.API.ConfigURI, configHandler(cfg)) 214 | r.GET(cfg.API.SysStatURI, sysStatsHandler()) 215 | r.POST(cfg.API.PushURI, pushHandler(cfg, q)) 216 | r.GET(cfg.API.MetricURI, metricsHandler) 217 | r.GET(cfg.API.HealthURI, heartbeatHandler) 218 | r.HEAD(cfg.API.HealthURI, heartbeatHandler) 219 | r.GET("/version", versionHandler) 220 | r.GET("/", rootHandler) 221 | 222 | return r 223 | } 224 | 225 | // markFailedNotification adds failure logs for all tokens in push notification 226 | func markFailedNotification( 227 | cfg *config.ConfYaml, 228 | notification *notify.PushNotification, 229 | reason string, 230 | ) []logx.LogPushEntry { 231 | logx.LogError.Error(reason) 232 | logs := make([]logx.LogPushEntry, 0) 233 | for _, token := range notification.Tokens { 234 | logs = append(logs, logx.GetLogPushEntry(&logx.InputLog{ 235 | ID: notification.ID, 236 | Status: core.FailedPush, 237 | Token: token, 238 | Message: notification.Message, 239 | Platform: notification.Platform, 240 | Error: errors.New(reason), 241 | HideToken: cfg.Log.HideToken, 242 | Format: cfg.Log.Format, 243 | })) 244 | } 245 | 246 | return logs 247 | } 248 | 249 | // HandleNotification add notification to queue list. 250 | func handleNotification( 251 | _ context.Context, 252 | cfg *config.ConfYaml, 253 | req notify.RequestPush, 254 | q *queue.Queue, 255 | ) (int, []logx.LogPushEntry) { 256 | var count int 257 | wg := sync.WaitGroup{} 258 | newNotification := []*notify.PushNotification{} 259 | 260 | if cfg.Core.Sync && !core.IsLocalQueue(core.Queue(cfg.Queue.Engine)) { 261 | cfg.Core.Sync = false 262 | } 263 | 264 | for i := range req.Notifications { 265 | notification := &req.Notifications[i] 266 | switch notification.Platform { 267 | case core.PlatFormIos: 268 | if !cfg.Ios.Enabled { 269 | continue 270 | } 271 | case core.PlatFormAndroid: 272 | if !cfg.Android.Enabled { 273 | continue 274 | } 275 | case core.PlatFormHuawei: 276 | if !cfg.Huawei.Enabled { 277 | continue 278 | } 279 | } 280 | newNotification = append(newNotification, notification) 281 | } 282 | 283 | logs := make([]logx.LogPushEntry, 0, count) 284 | for _, notification := range newNotification { 285 | if cfg.Core.Sync { 286 | wg.Add(1) 287 | } 288 | 289 | if core.IsLocalQueue(core.Queue(cfg.Queue.Engine)) && cfg.Core.Sync { 290 | func(msg *notify.PushNotification, cfg *config.ConfYaml) { 291 | if err := q.QueueTask(func(ctx context.Context) error { 292 | defer wg.Done() 293 | resp, err := notify.SendNotification(ctx, msg, cfg) 294 | if err != nil { 295 | return err 296 | } 297 | 298 | // add log 299 | logs = append(logs, resp.Logs...) 300 | 301 | return nil 302 | }); err != nil { 303 | logx.LogError.Error(err) 304 | } 305 | }(notification, cfg) 306 | } else if err := q.Queue(notification); err != nil { 307 | resp := markFailedNotification(cfg, notification, "max capacity reached") 308 | // add log 309 | logs = append(logs, resp...) 310 | wg.Done() 311 | } 312 | 313 | count += len(notification.Tokens) 314 | // Count topic message 315 | if notification.Topic != "" { 316 | count++ 317 | } 318 | } 319 | 320 | if cfg.Core.Sync { 321 | wg.Wait() 322 | } 323 | 324 | status.StatStorage.AddTotalCount(int64(count)) 325 | 326 | return count, logs 327 | } 328 | -------------------------------------------------------------------------------- /router/server_lambda.go: -------------------------------------------------------------------------------- 1 | //go:build lambda 2 | // +build lambda 3 | 4 | package router 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | 10 | "github.com/appleboy/gorush/config" 11 | "github.com/appleboy/gorush/logx" 12 | 13 | "github.com/apex/gateway" 14 | "github.com/golang-queue/queue" 15 | ) 16 | 17 | // RunHTTPServer provide run http or https protocol. 18 | func RunHTTPServer(ctx context.Context, cfg *config.ConfYaml, q *queue.Queue, s ...*http.Server) (err error) { 19 | if !cfg.Core.Enabled { 20 | logx.LogAccess.Debug("httpd server is disabled.") 21 | return nil 22 | } 23 | 24 | logx.LogAccess.Info("HTTPD server is running on " + cfg.Core.Port + " port.") 25 | 26 | return gateway.ListenAndServe(cfg.Core.Address+":"+cfg.Core.Port, routerEngine(cfg, q)) 27 | } 28 | -------------------------------------------------------------------------------- /router/server_normal.go: -------------------------------------------------------------------------------- 1 | //go:build !lambda 2 | // +build !lambda 3 | 4 | package router 5 | 6 | import ( 7 | "context" 8 | "crypto/tls" 9 | "encoding/base64" 10 | "errors" 11 | "net/http" 12 | "time" 13 | 14 | "github.com/appleboy/gorush/config" 15 | "github.com/appleboy/gorush/logx" 16 | 17 | "github.com/golang-queue/queue" 18 | "golang.org/x/sync/errgroup" 19 | ) 20 | 21 | // RunHTTPServer provide run http or https protocol. 22 | func RunHTTPServer(ctx context.Context, cfg *config.ConfYaml, q *queue.Queue, s ...*http.Server) (err error) { 23 | var server *http.Server 24 | 25 | if !cfg.Core.Enabled { 26 | logx.LogAccess.Info("httpd server is disabled.") 27 | return nil 28 | } 29 | 30 | if len(s) == 0 { 31 | //nolint:gosec 32 | server = &http.Server{ 33 | Addr: cfg.Core.Address + ":" + cfg.Core.Port, 34 | Handler: routerEngine(cfg, q), 35 | } 36 | } else { 37 | server = s[0] 38 | } 39 | 40 | logx.LogAccess.Info("HTTPD server is running on " + cfg.Core.Port + " port.") 41 | if cfg.Core.AutoTLS.Enabled { 42 | return startServer(ctx, autoTLSServer(cfg, q), cfg) 43 | } else if cfg.Core.SSL { 44 | config := &tls.Config{ 45 | MinVersion: tls.VersionTLS12, 46 | } 47 | 48 | if config.NextProtos == nil { 49 | config.NextProtos = []string{"http/1.1"} 50 | } 51 | 52 | config.Certificates = make([]tls.Certificate, 1) 53 | //nolint:gocritic 54 | if cfg.Core.CertPath != "" && cfg.Core.KeyPath != "" { 55 | config.Certificates[0], err = tls.LoadX509KeyPair(cfg.Core.CertPath, cfg.Core.KeyPath) 56 | if err != nil { 57 | logx.LogError.Error("Failed to load https cert file: ", err) 58 | return err 59 | } 60 | } else if cfg.Core.CertBase64 != "" && cfg.Core.KeyBase64 != "" { 61 | cert, err := base64.StdEncoding.DecodeString(cfg.Core.CertBase64) 62 | if err != nil { 63 | logx.LogError.Error("base64 decode error:", err.Error()) 64 | return err 65 | } 66 | key, err := base64.StdEncoding.DecodeString(cfg.Core.KeyBase64) 67 | if err != nil { 68 | logx.LogError.Error("base64 decode error:", err.Error()) 69 | return err 70 | } 71 | if config.Certificates[0], err = tls.X509KeyPair(cert, key); err != nil { 72 | logx.LogError.Error("tls key pair error:", err.Error()) 73 | return err 74 | } 75 | } else { 76 | return errors.New("missing https cert config") 77 | } 78 | 79 | server.TLSConfig = config 80 | } 81 | 82 | return startServer(ctx, server, cfg) 83 | } 84 | 85 | func listenAndServe(ctx context.Context, s *http.Server, cfg *config.ConfYaml) error { 86 | var g errgroup.Group 87 | g.Go(func() error { 88 | <-ctx.Done() 89 | timeout := time.Duration(cfg.Core.ShutdownTimeout) * time.Second 90 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 91 | defer cancel() 92 | return s.Shutdown(ctx) 93 | }) 94 | g.Go(func() error { 95 | if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { 96 | return err 97 | } 98 | return nil 99 | }) 100 | return g.Wait() 101 | } 102 | 103 | func listenAndServeTLS(ctx context.Context, s *http.Server, cfg *config.ConfYaml) error { 104 | var g errgroup.Group 105 | g.Go(func() error { 106 | <-ctx.Done() 107 | timeout := time.Duration(cfg.Core.ShutdownTimeout) * time.Second 108 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 109 | defer cancel() 110 | return s.Shutdown(ctx) 111 | }) 112 | g.Go(func() error { 113 | if err := s.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { 114 | return err 115 | } 116 | return nil 117 | }) 118 | return g.Wait() 119 | } 120 | 121 | func startServer(ctx context.Context, s *http.Server, cfg *config.ConfYaml) error { 122 | if s.TLSConfig == nil { 123 | return listenAndServe(ctx, s, cfg) 124 | } 125 | 126 | return listenAndServeTLS(ctx, s, cfg) 127 | } 128 | -------------------------------------------------------------------------------- /router/version.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | var ( 11 | version string 12 | commit string 13 | ) 14 | 15 | // SetVersion for setup version string. 16 | func SetVersion(ver string) { 17 | version = ver 18 | } 19 | 20 | // SetCommit for setup commit string. 21 | func SetCommit(ver string) { 22 | commit = ver 23 | } 24 | 25 | // GetVersion for get current version. 26 | func GetVersion() string { 27 | return version 28 | } 29 | 30 | // PrintGoRushVersion provide print server engine 31 | func PrintGoRushVersion() { 32 | if len(commit) > 7 { 33 | commit = commit[:7] 34 | } 35 | 36 | fmt.Printf(`GoRush %s, Commit: %s, Compiler: %s %s, Copyright (C) 2023 Bo-Yi Wu, Inc.`, 37 | version, 38 | commit, 39 | runtime.Compiler, 40 | runtime.Version()) 41 | fmt.Println() 42 | } 43 | 44 | // VersionMiddleware : add version on header. 45 | func VersionMiddleware() gin.HandlerFunc { 46 | // Set out header value for each response 47 | return func(c *gin.Context) { 48 | c.Header("X-GORUSH-VERSION", version) 49 | c.Next() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rpc/client_grpc_health.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/appleboy/gorush/core" 7 | "github.com/appleboy/gorush/rpc/proto" 8 | 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | // generate protobuffs 15 | // protoc --go_out=plugins=grpc,import_path=proto:. *.proto 16 | 17 | type healthClient struct { 18 | client proto.HealthClient 19 | conn *grpc.ClientConn 20 | } 21 | 22 | // NewGrpcHealthClient returns a new grpc Client. 23 | func NewGrpcHealthClient(conn *grpc.ClientConn) core.Health { 24 | client := new(healthClient) 25 | client.client = proto.NewHealthClient(conn) 26 | client.conn = conn 27 | return client 28 | } 29 | 30 | func (c *healthClient) Close() error { 31 | return c.conn.Close() 32 | } 33 | 34 | func (c *healthClient) Check(ctx context.Context) (bool, error) { 35 | var res *proto.HealthCheckResponse 36 | var err error 37 | req := new(proto.HealthCheckRequest) 38 | 39 | res, err = c.client.Check(ctx, req) 40 | if err == nil { 41 | if res.GetStatus() == proto.HealthCheckResponse_SERVING { 42 | return true, nil 43 | } 44 | return false, nil 45 | } 46 | //nolint:exhaustive 47 | switch status.Code(err) { 48 | case 49 | codes.Aborted, 50 | codes.DataLoss, 51 | codes.DeadlineExceeded, 52 | codes.Internal, 53 | codes.Unavailable: 54 | // non-fatal errors 55 | default: 56 | return false, err 57 | } 58 | 59 | return false, err 60 | } 61 | -------------------------------------------------------------------------------- /rpc/client_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | -------------------------------------------------------------------------------- /rpc/example/go/health/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/appleboy/gorush/rpc" 9 | 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials/insecure" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | const ( 16 | address = "localhost:9000" 17 | ) 18 | 19 | func main() { 20 | // Set up a connection to the server. 21 | conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) 22 | if err != nil { 23 | log.Fatalf("did not connect: %v", err) 24 | } 25 | defer conn.Close() 26 | 27 | client := rpc.NewGrpcHealthClient(conn) 28 | 29 | for { 30 | ok, err := client.Check(context.Background()) 31 | if !ok || err != nil { 32 | log.Printf("can't connect grpc server: %v, code: %v\n", err, status.Code(err)) 33 | } else { 34 | log.Println("connect the grpc server successfully") 35 | } 36 | 37 | <-time.After(time.Second) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rpc/example/go/send/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/appleboy/gorush/rpc/proto" 8 | 9 | structpb "github.com/golang/protobuf/ptypes/struct" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials/insecure" 12 | ) 13 | 14 | const ( 15 | address = "localhost:9000" 16 | ) 17 | 18 | func main() { 19 | // Set up a connection to the server. 20 | conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) 21 | if err != nil { 22 | log.Fatalf("did not connect: %v", err) 23 | } 24 | defer conn.Close() 25 | c := proto.NewGorushClient(conn) 26 | 27 | r, err := c.Send(context.Background(), &proto.NotificationRequest{ 28 | Platform: 2, 29 | Tokens: []string{"1234567890"}, 30 | Message: "test message", 31 | Badge: 1, 32 | Category: "test", 33 | Sound: "test", 34 | Priority: proto.NotificationRequest_HIGH, 35 | Alert: &proto.Alert{ 36 | Title: "Test Title", 37 | Body: "Test Alert Body", 38 | Subtitle: "Test Alert Sub Title", 39 | LocKey: "Test loc key", 40 | LocArgs: []string{"test", "test"}, 41 | }, 42 | Data: &structpb.Struct{ 43 | Fields: map[string]*structpb.Value{ 44 | "key1": { 45 | Kind: &structpb.Value_StringValue{StringValue: "welcome"}, 46 | }, 47 | "key2": { 48 | Kind: &structpb.Value_NumberValue{NumberValue: 2}, 49 | }, 50 | }, 51 | }, 52 | }) 53 | if err != nil { 54 | log.Println("could not greet: ", err) 55 | } 56 | 57 | if r != nil { 58 | log.Printf("Success: %t\n", r.Success) 59 | log.Printf("Count: %d\n", r.Counts) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rpc/example/node/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | npm-debug.log 4 | .yarn-cache 5 | -------------------------------------------------------------------------------- /rpc/example/node/README.md: -------------------------------------------------------------------------------- 1 | # gRPC in 3 minutes (Node.js) 2 | 3 | ## PREREQUISITES 4 | 5 | `node`: This requires Node 12.x or greater. 6 | 7 | ## INSTALL 8 | 9 | ```sh 10 | npm install 11 | npm install -g grpc-tools 12 | ``` 13 | 14 | ## Node gRPC protoc 15 | 16 | ```sh 17 | cd $GOPATH/src/github.com/appleboy/gorush 18 | protoc -I rpc/proto rpc/proto/gorush.proto --js_out=import_style=commonjs,binary:rpc/example/node/ --grpc_out=rpc/example/node/ --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` 19 | ``` 20 | -------------------------------------------------------------------------------- /rpc/example/node/client.js: -------------------------------------------------------------------------------- 1 | var messages = require('./gorush_pb'); 2 | var services = require('./gorush_grpc_pb'); 3 | 4 | var grpc = require('grpc'); 5 | 6 | function main() { 7 | var client = new services.GorushClient('localhost:9000', 8 | grpc.credentials.createInsecure()); 9 | var request = new messages.NotificationRequest(); 10 | var alert = new messages.Alert(); 11 | request.setPlatform(2); 12 | request.setTokensList(["1234567890"]); 13 | request.setMessage("Hello!!"); 14 | request.setTitle("hello2"); 15 | request.setBadge(2); 16 | request.setCategory("mycategory"); 17 | request.setSound("sound") 18 | alert.setTitle("title"); 19 | request.setAlert(alert); 20 | request.setThreadid("threadID"); 21 | request.setContentavailable(false); 22 | request.setMutablecontent(false); 23 | client.send(request, function (err, response) { 24 | if(err) { 25 | console.log(err); 26 | } else { 27 | console.log("Success:", response.getSuccess()); 28 | console.log("Counts:", response.getCounts()); 29 | } 30 | }); 31 | } 32 | 33 | main(); 34 | -------------------------------------------------------------------------------- /rpc/example/node/gorush_grpc_pb.js: -------------------------------------------------------------------------------- 1 | // GENERATED CODE -- DO NOT EDIT! 2 | 3 | 'use strict'; 4 | var grpc = require('grpc'); 5 | var gorush_pb = require('./gorush_pb.js'); 6 | var google_protobuf_struct_pb = require('google-protobuf/google/protobuf/struct_pb.js'); 7 | 8 | function serialize_proto_HealthCheckRequest(arg) { 9 | if (!(arg instanceof gorush_pb.HealthCheckRequest)) { 10 | throw new Error('Expected argument of type proto.HealthCheckRequest'); 11 | } 12 | return Buffer.from(arg.serializeBinary()); 13 | } 14 | 15 | function deserialize_proto_HealthCheckRequest(buffer_arg) { 16 | return gorush_pb.HealthCheckRequest.deserializeBinary(new Uint8Array(buffer_arg)); 17 | } 18 | 19 | function serialize_proto_HealthCheckResponse(arg) { 20 | if (!(arg instanceof gorush_pb.HealthCheckResponse)) { 21 | throw new Error('Expected argument of type proto.HealthCheckResponse'); 22 | } 23 | return Buffer.from(arg.serializeBinary()); 24 | } 25 | 26 | function deserialize_proto_HealthCheckResponse(buffer_arg) { 27 | return gorush_pb.HealthCheckResponse.deserializeBinary(new Uint8Array(buffer_arg)); 28 | } 29 | 30 | function serialize_proto_NotificationReply(arg) { 31 | if (!(arg instanceof gorush_pb.NotificationReply)) { 32 | throw new Error('Expected argument of type proto.NotificationReply'); 33 | } 34 | return Buffer.from(arg.serializeBinary()); 35 | } 36 | 37 | function deserialize_proto_NotificationReply(buffer_arg) { 38 | return gorush_pb.NotificationReply.deserializeBinary(new Uint8Array(buffer_arg)); 39 | } 40 | 41 | function serialize_proto_NotificationRequest(arg) { 42 | if (!(arg instanceof gorush_pb.NotificationRequest)) { 43 | throw new Error('Expected argument of type proto.NotificationRequest'); 44 | } 45 | return Buffer.from(arg.serializeBinary()); 46 | } 47 | 48 | function deserialize_proto_NotificationRequest(buffer_arg) { 49 | return gorush_pb.NotificationRequest.deserializeBinary(new Uint8Array(buffer_arg)); 50 | } 51 | 52 | 53 | var GorushService = exports.GorushService = { 54 | send: { 55 | path: '/proto.Gorush/Send', 56 | requestStream: false, 57 | responseStream: false, 58 | requestType: gorush_pb.NotificationRequest, 59 | responseType: gorush_pb.NotificationReply, 60 | requestSerialize: serialize_proto_NotificationRequest, 61 | requestDeserialize: deserialize_proto_NotificationRequest, 62 | responseSerialize: serialize_proto_NotificationReply, 63 | responseDeserialize: deserialize_proto_NotificationReply, 64 | }, 65 | }; 66 | 67 | exports.GorushClient = grpc.makeGenericClientConstructor(GorushService); 68 | var HealthService = exports.HealthService = { 69 | check: { 70 | path: '/proto.Health/Check', 71 | requestStream: false, 72 | responseStream: false, 73 | requestType: gorush_pb.HealthCheckRequest, 74 | responseType: gorush_pb.HealthCheckResponse, 75 | requestSerialize: serialize_proto_HealthCheckRequest, 76 | requestDeserialize: deserialize_proto_HealthCheckRequest, 77 | responseSerialize: serialize_proto_HealthCheckResponse, 78 | responseDeserialize: deserialize_proto_HealthCheckResponse, 79 | }, 80 | }; 81 | 82 | exports.HealthClient = grpc.makeGenericClientConstructor(HealthService); 83 | -------------------------------------------------------------------------------- /rpc/example/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gorush-examples", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "async": "^3.2.6", 6 | "global": "^4.4.0", 7 | "google-protobuf": "^3.21.4", 8 | "grpc": "^1.24.11", 9 | "lodash": "^4.17.21", 10 | "minimist": ">=1.2.8" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rpc/proto/gorush.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | import "google/protobuf/struct.proto"; 3 | 4 | package proto; 5 | option go_package = "./;proto"; 6 | 7 | message Alert { 8 | string title = 1; 9 | string body = 2; 10 | string subtitle = 3; 11 | string action = 4; 12 | string actionLocKey = 5; 13 | string launchImage = 6; 14 | string locKey = 7; 15 | string titleLocKey = 8; 16 | repeated string locArgs = 9; 17 | repeated string titleLocArgs = 10; 18 | } 19 | 20 | message NotificationRequest { 21 | repeated string tokens = 1; 22 | int32 platform = 2; 23 | string message = 3; 24 | string title = 4; 25 | string topic = 5; 26 | string key = 6; 27 | int32 badge = 7; 28 | string category = 8; 29 | Alert alert = 9; 30 | string sound = 10; 31 | bool contentAvailable = 11; 32 | string threadID = 12; 33 | bool mutableContent = 13; 34 | google.protobuf.Struct data = 14; 35 | string image = 15; 36 | enum Priority { 37 | NORMAL = 0; 38 | HIGH = 1; 39 | } 40 | Priority priority = 16; 41 | string ID = 17; 42 | string pushType = 18; 43 | // default is production 44 | bool development = 19; 45 | } 46 | 47 | message NotificationReply { 48 | bool success = 1; 49 | int32 counts = 2; 50 | } 51 | 52 | service Gorush { 53 | rpc Send (NotificationRequest) returns (NotificationReply) {} 54 | } 55 | 56 | message HealthCheckRequest { 57 | string service = 1; 58 | } 59 | 60 | message HealthCheckResponse { 61 | enum ServingStatus { 62 | UNKNOWN = 0; 63 | SERVING = 1; 64 | NOT_SERVING = 2; 65 | } 66 | ServingStatus status = 1; 67 | } 68 | 69 | service Health { 70 | rpc Check(HealthCheckRequest) returns (HealthCheckResponse); 71 | } 72 | -------------------------------------------------------------------------------- /rpc/proto/gorush_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v3.20.3 5 | // source: gorush.proto 6 | 7 | package proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.64.0 or later. 19 | const _ = grpc.SupportPackageIsVersion9 20 | 21 | const ( 22 | Gorush_Send_FullMethodName = "/proto.Gorush/Send" 23 | ) 24 | 25 | // GorushClient is the client API for Gorush service. 26 | // 27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 28 | type GorushClient interface { 29 | Send(ctx context.Context, in *NotificationRequest, opts ...grpc.CallOption) (*NotificationReply, error) 30 | } 31 | 32 | type gorushClient struct { 33 | cc grpc.ClientConnInterface 34 | } 35 | 36 | func NewGorushClient(cc grpc.ClientConnInterface) GorushClient { 37 | return &gorushClient{cc} 38 | } 39 | 40 | func (c *gorushClient) Send(ctx context.Context, in *NotificationRequest, opts ...grpc.CallOption) (*NotificationReply, error) { 41 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 42 | out := new(NotificationReply) 43 | err := c.cc.Invoke(ctx, Gorush_Send_FullMethodName, in, out, cOpts...) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return out, nil 48 | } 49 | 50 | // GorushServer is the server API for Gorush service. 51 | // All implementations should embed UnimplementedGorushServer 52 | // for forward compatibility. 53 | type GorushServer interface { 54 | Send(context.Context, *NotificationRequest) (*NotificationReply, error) 55 | } 56 | 57 | // UnimplementedGorushServer should be embedded to have 58 | // forward compatible implementations. 59 | // 60 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 61 | // pointer dereference when methods are called. 62 | type UnimplementedGorushServer struct{} 63 | 64 | func (UnimplementedGorushServer) Send(context.Context, *NotificationRequest) (*NotificationReply, error) { 65 | return nil, status.Errorf(codes.Unimplemented, "method Send not implemented") 66 | } 67 | func (UnimplementedGorushServer) testEmbeddedByValue() {} 68 | 69 | // UnsafeGorushServer may be embedded to opt out of forward compatibility for this service. 70 | // Use of this interface is not recommended, as added methods to GorushServer will 71 | // result in compilation errors. 72 | type UnsafeGorushServer interface { 73 | mustEmbedUnimplementedGorushServer() 74 | } 75 | 76 | func RegisterGorushServer(s grpc.ServiceRegistrar, srv GorushServer) { 77 | // If the following call pancis, it indicates UnimplementedGorushServer was 78 | // embedded by pointer and is nil. This will cause panics if an 79 | // unimplemented method is ever invoked, so we test this at initialization 80 | // time to prevent it from happening at runtime later due to I/O. 81 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 82 | t.testEmbeddedByValue() 83 | } 84 | s.RegisterService(&Gorush_ServiceDesc, srv) 85 | } 86 | 87 | func _Gorush_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 88 | in := new(NotificationRequest) 89 | if err := dec(in); err != nil { 90 | return nil, err 91 | } 92 | if interceptor == nil { 93 | return srv.(GorushServer).Send(ctx, in) 94 | } 95 | info := &grpc.UnaryServerInfo{ 96 | Server: srv, 97 | FullMethod: Gorush_Send_FullMethodName, 98 | } 99 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 100 | return srv.(GorushServer).Send(ctx, req.(*NotificationRequest)) 101 | } 102 | return interceptor(ctx, in, info, handler) 103 | } 104 | 105 | // Gorush_ServiceDesc is the grpc.ServiceDesc for Gorush service. 106 | // It's only intended for direct use with grpc.RegisterService, 107 | // and not to be introspected or modified (even as a copy) 108 | var Gorush_ServiceDesc = grpc.ServiceDesc{ 109 | ServiceName: "proto.Gorush", 110 | HandlerType: (*GorushServer)(nil), 111 | Methods: []grpc.MethodDesc{ 112 | { 113 | MethodName: "Send", 114 | Handler: _Gorush_Send_Handler, 115 | }, 116 | }, 117 | Streams: []grpc.StreamDesc{}, 118 | Metadata: "gorush.proto", 119 | } 120 | 121 | const ( 122 | Health_Check_FullMethodName = "/proto.Health/Check" 123 | ) 124 | 125 | // HealthClient is the client API for Health service. 126 | // 127 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 128 | type HealthClient interface { 129 | Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) 130 | } 131 | 132 | type healthClient struct { 133 | cc grpc.ClientConnInterface 134 | } 135 | 136 | func NewHealthClient(cc grpc.ClientConnInterface) HealthClient { 137 | return &healthClient{cc} 138 | } 139 | 140 | func (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { 141 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 142 | out := new(HealthCheckResponse) 143 | err := c.cc.Invoke(ctx, Health_Check_FullMethodName, in, out, cOpts...) 144 | if err != nil { 145 | return nil, err 146 | } 147 | return out, nil 148 | } 149 | 150 | // HealthServer is the server API for Health service. 151 | // All implementations should embed UnimplementedHealthServer 152 | // for forward compatibility. 153 | type HealthServer interface { 154 | Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) 155 | } 156 | 157 | // UnimplementedHealthServer should be embedded to have 158 | // forward compatible implementations. 159 | // 160 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 161 | // pointer dereference when methods are called. 162 | type UnimplementedHealthServer struct{} 163 | 164 | func (UnimplementedHealthServer) Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { 165 | return nil, status.Errorf(codes.Unimplemented, "method Check not implemented") 166 | } 167 | func (UnimplementedHealthServer) testEmbeddedByValue() {} 168 | 169 | // UnsafeHealthServer may be embedded to opt out of forward compatibility for this service. 170 | // Use of this interface is not recommended, as added methods to HealthServer will 171 | // result in compilation errors. 172 | type UnsafeHealthServer interface { 173 | mustEmbedUnimplementedHealthServer() 174 | } 175 | 176 | func RegisterHealthServer(s grpc.ServiceRegistrar, srv HealthServer) { 177 | // If the following call pancis, it indicates UnimplementedHealthServer was 178 | // embedded by pointer and is nil. This will cause panics if an 179 | // unimplemented method is ever invoked, so we test this at initialization 180 | // time to prevent it from happening at runtime later due to I/O. 181 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 182 | t.testEmbeddedByValue() 183 | } 184 | s.RegisterService(&Health_ServiceDesc, srv) 185 | } 186 | 187 | func _Health_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 188 | in := new(HealthCheckRequest) 189 | if err := dec(in); err != nil { 190 | return nil, err 191 | } 192 | if interceptor == nil { 193 | return srv.(HealthServer).Check(ctx, in) 194 | } 195 | info := &grpc.UnaryServerInfo{ 196 | Server: srv, 197 | FullMethod: Health_Check_FullMethodName, 198 | } 199 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 200 | return srv.(HealthServer).Check(ctx, req.(*HealthCheckRequest)) 201 | } 202 | return interceptor(ctx, in, info, handler) 203 | } 204 | 205 | // Health_ServiceDesc is the grpc.ServiceDesc for Health service. 206 | // It's only intended for direct use with grpc.RegisterService, 207 | // and not to be introspected or modified (even as a copy) 208 | var Health_ServiceDesc = grpc.ServiceDesc{ 209 | ServiceName: "proto.Health", 210 | HandlerType: (*HealthServer)(nil), 211 | Methods: []grpc.MethodDesc{ 212 | { 213 | MethodName: "Check", 214 | Handler: _Health_Check_Handler, 215 | }, 216 | }, 217 | Streams: []grpc.StreamDesc{}, 218 | Metadata: "gorush.proto", 219 | } 220 | -------------------------------------------------------------------------------- /rpc/server.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "math" 9 | "net" 10 | "runtime/debug" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/appleboy/gorush/config" 15 | "github.com/appleboy/gorush/core" 16 | "github.com/appleboy/gorush/logx" 17 | "github.com/appleboy/gorush/notify" 18 | "github.com/appleboy/gorush/rpc/proto" 19 | 20 | grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" 21 | grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" 22 | grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 23 | "go.opencensus.io/plugin/ocgrpc" 24 | "google.golang.org/grpc" 25 | "google.golang.org/grpc/codes" 26 | "google.golang.org/grpc/credentials" 27 | "google.golang.org/grpc/reflection" 28 | "google.golang.org/grpc/status" 29 | ) 30 | 31 | // Server is used to implement gorush grpc server. 32 | type Server struct { 33 | cfg *config.ConfYaml 34 | mu sync.Mutex 35 | // statusMap stores the serving status of the services this Server monitors. 36 | statusMap map[string]proto.HealthCheckResponse_ServingStatus 37 | } 38 | 39 | // NewServer returns a new Server. 40 | func NewServer(cfg *config.ConfYaml) *Server { 41 | return &Server{ 42 | cfg: cfg, 43 | statusMap: make(map[string]proto.HealthCheckResponse_ServingStatus), 44 | } 45 | } 46 | 47 | // Check implements `service Health`. 48 | func (s *Server) Check(ctx context.Context, in *proto.HealthCheckRequest) (*proto.HealthCheckResponse, error) { 49 | s.mu.Lock() 50 | defer s.mu.Unlock() 51 | if in.Service == "" { 52 | // check the server overall health status. 53 | return &proto.HealthCheckResponse{ 54 | Status: proto.HealthCheckResponse_SERVING, 55 | }, nil 56 | } 57 | if status, ok := s.statusMap[in.Service]; ok { 58 | return &proto.HealthCheckResponse{ 59 | Status: status, 60 | }, nil 61 | } 62 | return nil, status.Error(codes.NotFound, "unknown service") 63 | } 64 | 65 | // Send implements helloworld.GreeterServer 66 | func (s *Server) Send(ctx context.Context, in *proto.NotificationRequest) (*proto.NotificationReply, error) { 67 | badge := int(in.Badge) 68 | notification := notify.PushNotification{ 69 | ID: in.ID, 70 | Platform: int(in.Platform), 71 | Tokens: in.Tokens, 72 | Message: in.Message, 73 | Title: in.Title, 74 | Topic: in.Topic, 75 | Category: in.Category, 76 | Sound: in.Sound, 77 | ContentAvailable: in.ContentAvailable, 78 | ThreadID: in.ThreadID, 79 | MutableContent: in.MutableContent, 80 | Image: in.Image, 81 | Priority: strings.ToLower(in.GetPriority().String()), 82 | PushType: in.PushType, 83 | Development: in.Development, 84 | } 85 | 86 | if badge > 0 { 87 | notification.Badge = &badge 88 | } 89 | 90 | if in.Topic != "" && in.Platform == core.PlatFormAndroid { 91 | notification.Topic = in.Topic 92 | } 93 | 94 | if in.Alert != nil { 95 | notification.Alert = notify.Alert{ 96 | Title: in.Alert.Title, 97 | Body: in.Alert.Body, 98 | Subtitle: in.Alert.Subtitle, 99 | Action: in.Alert.Action, 100 | ActionLocKey: in.Alert.Action, 101 | LaunchImage: in.Alert.LaunchImage, 102 | LocArgs: in.Alert.LocArgs, 103 | LocKey: in.Alert.LocKey, 104 | TitleLocArgs: in.Alert.TitleLocArgs, 105 | TitleLocKey: in.Alert.TitleLocKey, 106 | } 107 | } 108 | 109 | if in.Data != nil { 110 | notification.Data = in.Data.AsMap() 111 | } 112 | 113 | go func() { 114 | ctx := context.Background() 115 | _, err := notify.SendNotification(ctx, ¬ification, s.cfg) 116 | if err != nil { 117 | logx.LogError.Error(err) 118 | } 119 | }() 120 | 121 | counts, err := safeIntToInt32(len(notification.Tokens)) 122 | if err != nil { 123 | return nil, status.Error(codes.InvalidArgument, err.Error()) 124 | } 125 | 126 | return &proto.NotificationReply{ 127 | Success: true, 128 | Counts: counts, 129 | }, nil 130 | } 131 | 132 | // safeIntToInt32 converts an int to an int32, returning an error if the int is out of range. 133 | func safeIntToInt32(n int) (int32, error) { 134 | if n < math.MinInt32 || n > math.MaxInt32 { 135 | return 0, errors.New("integer overflow: value out of int32 range") 136 | } 137 | return int32(n), nil 138 | } 139 | 140 | // RunGRPCServer run gorush grpc server 141 | func RunGRPCServer(ctx context.Context, cfg *config.ConfYaml) error { 142 | if !cfg.GRPC.Enabled { 143 | logx.LogAccess.Info("gRPC server is disabled.") 144 | return nil 145 | } 146 | 147 | recoveryOpt := grpc_recovery.WithRecoveryHandlerContext( 148 | func(ctx context.Context, p interface{}) error { 149 | fmt.Printf("[PANIC] %s\n%s", p, string(debug.Stack())) 150 | return status.Error(codes.Internal, "system has been broken") 151 | }, 152 | ) 153 | 154 | unaryInterceptors := []grpc.UnaryServerInterceptor{ 155 | grpc_prometheus.UnaryServerInterceptor, 156 | grpc_recovery.UnaryServerInterceptor(recoveryOpt), 157 | } 158 | 159 | var s *grpc.Server 160 | 161 | if cfg.Core.SSL && cfg.Core.CertPath != "" && cfg.Core.KeyPath != "" { 162 | tlsCert, err := tls.LoadX509KeyPair(cfg.Core.CertPath, cfg.Core.KeyPath) 163 | if err != nil { 164 | logx.LogError.Error("failed to load tls cert file: ", err) 165 | return err 166 | } 167 | 168 | tlsConfig := &tls.Config{ 169 | Certificates: []tls.Certificate{tlsCert}, 170 | ClientAuth: tls.NoClientCert, 171 | MinVersion: tls.VersionTLS12, // Set minimum TLS version to TLS 1.2 172 | } 173 | 174 | s = grpc.NewServer( 175 | grpc.Creds(credentials.NewTLS(tlsConfig)), 176 | grpc.StatsHandler(&ocgrpc.ServerHandler{}), 177 | grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unaryInterceptors...)), 178 | ) 179 | } else { 180 | s = grpc.NewServer( 181 | grpc.StatsHandler(&ocgrpc.ServerHandler{}), 182 | grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unaryInterceptors...)), 183 | ) 184 | } 185 | 186 | rpcSrv := NewServer(cfg) 187 | proto.RegisterGorushServer(s, rpcSrv) 188 | proto.RegisterHealthServer(s, rpcSrv) 189 | 190 | // Register reflection service on gRPC server. 191 | reflection.Register(s) 192 | 193 | lis, err := net.Listen("tcp", ":"+cfg.GRPC.Port) 194 | if err != nil { 195 | logx.LogError.Fatalln(err) 196 | return err 197 | } 198 | logx.LogAccess.Info("gRPC server is running on " + cfg.GRPC.Port + " port.") 199 | go func() { 200 | <-ctx.Done() 201 | s.GracefulStop() // graceful shutdown 202 | logx.LogAccess.Info("shutdown the gRPC server") 203 | }() 204 | if err = s.Serve(lis); err != nil { 205 | logx.LogError.Fatalln(err) 206 | } 207 | return err 208 | } 209 | -------------------------------------------------------------------------------- /rpc/server_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestSafeIntToInt32(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | input int 12 | want int32 13 | wantErr bool 14 | }{ 15 | {"Valid int32", 123, 123, false}, 16 | {"Max int32", math.MaxInt32, math.MaxInt32, false}, 17 | {"Min int32", math.MinInt32, math.MinInt32, false}, 18 | {"Overflow int32", math.MaxInt32 + 1, 0, true}, 19 | {"Underflow int32", math.MinInt32 - 1, 0, true}, 20 | } 21 | 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | got, err := safeIntToInt32(tt.input) 25 | if (err != nil) != tt.wantErr { 26 | t.Errorf("safeIntToInt32() error = %v, wantErr %v", err, tt.wantErr) 27 | return 28 | } 29 | if got != tt.want { 30 | t.Errorf("safeIntToInt32() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | // const gRPCAddr = "localhost:9000" 37 | 38 | // func initTest() *config.ConfYaml { 39 | // cfg, _ := config.LoadConf() 40 | // cfg.Core.Mode = "test" 41 | // return cfg 42 | // } 43 | 44 | // func TestGracefulShutDownGRPCServer(t *testing.T) { 45 | // cfg := initTest() 46 | // cfg.GRPC.Enabled = true 47 | // cfg.GRPC.Port = "9000" 48 | // cfg.Log.Format = "json" 49 | 50 | // // Run gRPC server 51 | // ctx, gRPCContextCancel := context.WithCancel(context.Background()) 52 | // go func() { 53 | // if err := RunGRPCServer(ctx, cfg); err != nil { 54 | // panic(err) 55 | // } 56 | // }() 57 | 58 | // // gRPC client conn 59 | // conn, err := grpc.Dial( 60 | // gRPCAddr, 61 | // grpc.WithTransportCredentials(insecure.NewCredentials()), 62 | // grpc.WithDefaultCallOptions(grpc.WaitForReady(true)), 63 | // ) // wait for server ready 64 | // if err != nil { 65 | // t.Error(err) 66 | // } 67 | 68 | // // Stop gRPC server 69 | // go gRPCContextCancel() 70 | 71 | // // wait for client connection would be closed 72 | // for conn.GetState() != connectivity.TransientFailure { 73 | // } 74 | // conn.Close() 75 | // } 76 | -------------------------------------------------------------------------------- /screenshot/lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gorush/316fa2605d16e50a32415560ab81d81b5a9bd1f9/screenshot/lambda.png -------------------------------------------------------------------------------- /screenshot/memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gorush/316fa2605d16e50a32415560ab81d81b5a9bd1f9/screenshot/memory.png -------------------------------------------------------------------------------- /screenshot/metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gorush/316fa2605d16e50a32415560ab81d81b5a9bd1f9/screenshot/metrics.png -------------------------------------------------------------------------------- /screenshot/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gorush/316fa2605d16e50a32415560ab81d81b5a9bd1f9/screenshot/status.png -------------------------------------------------------------------------------- /status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/appleboy/gorush/config" 7 | "github.com/appleboy/gorush/core" 8 | "github.com/appleboy/gorush/logx" 9 | "github.com/appleboy/gorush/storage/badger" 10 | "github.com/appleboy/gorush/storage/boltdb" 11 | "github.com/appleboy/gorush/storage/buntdb" 12 | "github.com/appleboy/gorush/storage/leveldb" 13 | "github.com/appleboy/gorush/storage/memory" 14 | "github.com/appleboy/gorush/storage/redis" 15 | 16 | "github.com/thoas/stats" 17 | ) 18 | 19 | // Stats provide response time, status code count, etc. 20 | var Stats *stats.Stats 21 | 22 | // StatStorage implements the storage interface 23 | var StatStorage *StateStorage 24 | 25 | // App is status structure 26 | type App struct { 27 | Version string `json:"version"` 28 | BusyWorkers int64 `json:"busy_workers"` 29 | SuccessTasks uint64 `json:"success_tasks"` 30 | FailureTasks uint64 `json:"failure_tasks"` 31 | SubmittedTasks uint64 `json:"submitted_tasks"` 32 | TotalCount int64 `json:"total_count"` 33 | Ios IosStatus `json:"ios"` 34 | Android AndroidStatus `json:"android"` 35 | Huawei HuaweiStatus `json:"huawei"` 36 | } 37 | 38 | // AndroidStatus is android structure 39 | type AndroidStatus struct { 40 | PushSuccess int64 `json:"push_success"` 41 | PushError int64 `json:"push_error"` 42 | } 43 | 44 | // IosStatus is iOS structure 45 | type IosStatus struct { 46 | PushSuccess int64 `json:"push_success"` 47 | PushError int64 `json:"push_error"` 48 | } 49 | 50 | // HuaweiStatus is huawei structure 51 | type HuaweiStatus struct { 52 | PushSuccess int64 `json:"push_success"` 53 | PushError int64 `json:"push_error"` 54 | } 55 | 56 | // InitAppStatus for initialize app status 57 | func InitAppStatus(conf *config.ConfYaml) error { 58 | logx.LogAccess.Info("Init App Status Engine as ", conf.Stat.Engine) 59 | 60 | var store core.Storage 61 | //nolint:goconst 62 | switch conf.Stat.Engine { 63 | case "memory": 64 | store = memory.New() 65 | case "redis": 66 | store = redis.New( 67 | conf.Stat.Redis.Addr, 68 | conf.Stat.Redis.Username, 69 | conf.Stat.Redis.Password, 70 | conf.Stat.Redis.DB, 71 | conf.Stat.Redis.Cluster, 72 | ) 73 | case "boltdb": 74 | store = boltdb.New( 75 | conf.Stat.BoltDB.Path, 76 | conf.Stat.BoltDB.Bucket, 77 | ) 78 | case "buntdb": 79 | store = buntdb.New( 80 | conf.Stat.BuntDB.Path, 81 | ) 82 | case "leveldb": 83 | store = leveldb.New( 84 | conf.Stat.LevelDB.Path, 85 | ) 86 | case "badger": 87 | store = badger.New( 88 | conf.Stat.BadgerDB.Path, 89 | ) 90 | default: 91 | logx.LogError.Error("storage error: can't find storage driver") 92 | return errors.New("can't find storage driver") 93 | } 94 | 95 | StatStorage = NewStateStorage(store) 96 | 97 | if err := StatStorage.Init(); err != nil { 98 | logx.LogError.Error("storage error: " + err.Error()) 99 | 100 | return err 101 | } 102 | 103 | Stats = stats.New() 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /status/status_test.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/appleboy/gorush/config" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | os.Exit(m.Run()) 15 | } 16 | 17 | func TestStorageDriverExist(t *testing.T) { 18 | cfg, _ := config.LoadConf() 19 | cfg.Stat.Engine = "Test" 20 | err := InitAppStatus(cfg) 21 | assert.Error(t, err) 22 | } 23 | 24 | func TestStatForMemoryEngine(t *testing.T) { 25 | // wait android push notification response. 26 | time.Sleep(5 * time.Second) 27 | 28 | var val int64 29 | cfg, _ := config.LoadConf() 30 | cfg.Stat.Engine = "memory" 31 | err := InitAppStatus(cfg) 32 | assert.Nil(t, err) 33 | 34 | StatStorage.AddTotalCount(100) 35 | StatStorage.AddIosSuccess(200) 36 | StatStorage.AddIosError(300) 37 | StatStorage.AddAndroidSuccess(400) 38 | StatStorage.AddAndroidError(500) 39 | 40 | val = StatStorage.GetTotalCount() 41 | assert.Equal(t, int64(100), val) 42 | val = StatStorage.GetIosSuccess() 43 | assert.Equal(t, int64(200), val) 44 | val = StatStorage.GetIosError() 45 | assert.Equal(t, int64(300), val) 46 | val = StatStorage.GetAndroidSuccess() 47 | assert.Equal(t, int64(400), val) 48 | val = StatStorage.GetAndroidError() 49 | assert.Equal(t, int64(500), val) 50 | } 51 | 52 | func TestRedisServerSuccess(t *testing.T) { 53 | cfg, _ := config.LoadConf() 54 | cfg.Stat.Engine = "redis" 55 | cfg.Stat.Redis.Addr = "redis:6379" 56 | 57 | err := InitAppStatus(cfg) 58 | 59 | assert.NoError(t, err) 60 | } 61 | 62 | func TestRedisServerError(t *testing.T) { 63 | cfg, _ := config.LoadConf() 64 | cfg.Stat.Engine = "redis" 65 | cfg.Stat.Redis.Addr = "redis:6370" 66 | 67 | err := InitAppStatus(cfg) 68 | 69 | assert.Error(t, err) 70 | } 71 | 72 | func TestStatForRedisEngine(t *testing.T) { 73 | var val int64 74 | cfg, _ := config.LoadConf() 75 | cfg.Stat.Engine = "redis" 76 | cfg.Stat.Redis.Addr = "redis:6379" 77 | err := InitAppStatus(cfg) 78 | assert.Nil(t, err) 79 | 80 | assert.Nil(t, StatStorage.Init()) 81 | StatStorage.Reset() 82 | 83 | StatStorage.AddTotalCount(100) 84 | StatStorage.AddIosSuccess(200) 85 | StatStorage.AddIosError(300) 86 | StatStorage.AddAndroidSuccess(400) 87 | StatStorage.AddAndroidError(500) 88 | 89 | val = StatStorage.GetTotalCount() 90 | assert.Equal(t, int64(100), val) 91 | val = StatStorage.GetIosSuccess() 92 | assert.Equal(t, int64(200), val) 93 | val = StatStorage.GetIosError() 94 | assert.Equal(t, int64(300), val) 95 | val = StatStorage.GetAndroidSuccess() 96 | assert.Equal(t, int64(400), val) 97 | val = StatStorage.GetAndroidError() 98 | assert.Equal(t, int64(500), val) 99 | } 100 | 101 | func TestDefaultEngine(t *testing.T) { 102 | var val int64 103 | // defaul engine as memory 104 | cfg, _ := config.LoadConf() 105 | err := InitAppStatus(cfg) 106 | assert.Nil(t, err) 107 | 108 | StatStorage.Reset() 109 | 110 | StatStorage.AddTotalCount(100) 111 | StatStorage.AddIosSuccess(200) 112 | StatStorage.AddIosError(300) 113 | StatStorage.AddAndroidSuccess(400) 114 | StatStorage.AddAndroidError(500) 115 | 116 | val = StatStorage.GetTotalCount() 117 | assert.Equal(t, int64(100), val) 118 | val = StatStorage.GetIosSuccess() 119 | assert.Equal(t, int64(200), val) 120 | val = StatStorage.GetIosError() 121 | assert.Equal(t, int64(300), val) 122 | val = StatStorage.GetAndroidSuccess() 123 | assert.Equal(t, int64(400), val) 124 | val = StatStorage.GetAndroidError() 125 | assert.Equal(t, int64(500), val) 126 | } 127 | 128 | func TestStatForBoltDBEngine(t *testing.T) { 129 | var val int64 130 | cfg, _ := config.LoadConf() 131 | cfg.Stat.Engine = "boltdb" 132 | err := InitAppStatus(cfg) 133 | assert.Nil(t, err) 134 | 135 | StatStorage.Reset() 136 | 137 | StatStorage.AddTotalCount(100) 138 | StatStorage.AddIosSuccess(200) 139 | StatStorage.AddIosError(300) 140 | StatStorage.AddAndroidSuccess(400) 141 | StatStorage.AddAndroidError(500) 142 | 143 | val = StatStorage.GetTotalCount() 144 | assert.Equal(t, int64(100), val) 145 | val = StatStorage.GetIosSuccess() 146 | assert.Equal(t, int64(200), val) 147 | val = StatStorage.GetIosError() 148 | assert.Equal(t, int64(300), val) 149 | val = StatStorage.GetAndroidSuccess() 150 | assert.Equal(t, int64(400), val) 151 | val = StatStorage.GetAndroidError() 152 | assert.Equal(t, int64(500), val) 153 | } 154 | 155 | // func TestStatForBuntDBEngine(t *testing.T) { 156 | // var val int64 157 | // cfg.Stat.Engine = "buntdb" 158 | // err := InitAppStatus() 159 | // assert.Nil(t, err) 160 | 161 | // StatStorage.Reset() 162 | 163 | // StatStorage.AddTotalCount(100) 164 | // StatStorage.AddIosSuccess(200) 165 | // StatStorage.AddIosError(300) 166 | // StatStorage.AddAndroidSuccess(400) 167 | // StatStorage.AddAndroidError(500) 168 | 169 | // val = StatStorage.GetTotalCount() 170 | // assert.Equal(t, int64(100), val) 171 | // val = StatStorage.GetIosSuccess() 172 | // assert.Equal(t, int64(200), val) 173 | // val = StatStorage.GetIosError() 174 | // assert.Equal(t, int64(300), val) 175 | // val = StatStorage.GetAndroidSuccess() 176 | // assert.Equal(t, int64(400), val) 177 | // val = StatStorage.GetAndroidError() 178 | // assert.Equal(t, int64(500), val) 179 | // } 180 | 181 | // func TestStatForLevelDBEngine(t *testing.T) { 182 | // var val int64 183 | // cfg.Stat.Engine = "leveldb" 184 | // err := InitAppStatus() 185 | // assert.Nil(t, err) 186 | 187 | // StatStorage.Reset() 188 | 189 | // StatStorage.AddTotalCount(100) 190 | // StatStorage.AddIosSuccess(200) 191 | // StatStorage.AddIosError(300) 192 | // StatStorage.AddAndroidSuccess(400) 193 | // StatStorage.AddAndroidError(500) 194 | 195 | // val = StatStorage.GetTotalCount() 196 | // assert.Equal(t, int64(100), val) 197 | // val = StatStorage.GetIosSuccess() 198 | // assert.Equal(t, int64(200), val) 199 | // val = StatStorage.GetIosError() 200 | // assert.Equal(t, int64(300), val) 201 | // val = StatStorage.GetAndroidSuccess() 202 | // assert.Equal(t, int64(400), val) 203 | // val = StatStorage.GetAndroidError() 204 | // assert.Equal(t, int64(500), val) 205 | // } 206 | 207 | // func TestStatForBadgerEngine(t *testing.T) { 208 | // var val int64 209 | // cfg.Stat.Engine = "badger" 210 | // err := InitAppStatus() 211 | // assert.Nil(t, err) 212 | 213 | // StatStorage.Reset() 214 | 215 | // StatStorage.AddTotalCount(100) 216 | // StatStorage.AddIosSuccess(200) 217 | // StatStorage.AddIosError(300) 218 | // StatStorage.AddAndroidSuccess(400) 219 | // StatStorage.AddAndroidError(500) 220 | 221 | // val = StatStorage.GetTotalCount() 222 | // assert.Equal(t, int64(100), val) 223 | // val = StatStorage.GetIosSuccess() 224 | // assert.Equal(t, int64(200), val) 225 | // val = StatStorage.GetIosError() 226 | // assert.Equal(t, int64(300), val) 227 | // val = StatStorage.GetAndroidSuccess() 228 | // assert.Equal(t, int64(400), val) 229 | // val = StatStorage.GetAndroidError() 230 | // assert.Equal(t, int64(500), val) 231 | // } 232 | -------------------------------------------------------------------------------- /status/storage.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "github.com/appleboy/gorush/core" 5 | ) 6 | 7 | type StateStorage struct { 8 | store core.Storage 9 | } 10 | 11 | func NewStateStorage(store core.Storage) *StateStorage { 12 | return &StateStorage{ 13 | store: store, 14 | } 15 | } 16 | 17 | func (s *StateStorage) Init() error { 18 | return s.store.Init() 19 | } 20 | 21 | func (s *StateStorage) Close() error { 22 | return s.store.Close() 23 | } 24 | 25 | // Reset Client storage. 26 | func (s *StateStorage) Reset() { 27 | s.store.Set(core.TotalCountKey, 0) 28 | s.store.Set(core.IosSuccessKey, 0) 29 | s.store.Set(core.IosErrorKey, 0) 30 | s.store.Set(core.AndroidSuccessKey, 0) 31 | s.store.Set(core.AndroidErrorKey, 0) 32 | s.store.Set(core.HuaweiSuccessKey, 0) 33 | s.store.Set(core.HuaweiErrorKey, 0) 34 | } 35 | 36 | // AddTotalCount record push notification count. 37 | func (s *StateStorage) AddTotalCount(count int64) { 38 | s.store.Add(core.TotalCountKey, count) 39 | } 40 | 41 | // AddIosSuccess record counts of success iOS push notification. 42 | func (s *StateStorage) AddIosSuccess(count int64) { 43 | s.store.Add(core.IosSuccessKey, count) 44 | } 45 | 46 | // AddIosError record counts of error iOS push notification. 47 | func (s *StateStorage) AddIosError(count int64) { 48 | s.store.Add(core.IosErrorKey, count) 49 | } 50 | 51 | // AddAndroidSuccess record counts of success Android push notification. 52 | func (s *StateStorage) AddAndroidSuccess(count int64) { 53 | s.store.Add(core.AndroidSuccessKey, count) 54 | } 55 | 56 | // AddAndroidError record counts of error Android push notification. 57 | func (s *StateStorage) AddAndroidError(count int64) { 58 | s.store.Add(core.AndroidErrorKey, count) 59 | } 60 | 61 | // AddHuaweiSuccess record counts of success Huawei push notification. 62 | func (s *StateStorage) AddHuaweiSuccess(count int64) { 63 | s.store.Add(core.HuaweiSuccessKey, count) 64 | } 65 | 66 | // AddHuaweiError record counts of error Huawei push notification. 67 | func (s *StateStorage) AddHuaweiError(count int64) { 68 | s.store.Add(core.HuaweiErrorKey, count) 69 | } 70 | 71 | // GetTotalCount show counts of all notification. 72 | func (s *StateStorage) GetTotalCount() int64 { 73 | return s.store.Get(core.TotalCountKey) 74 | } 75 | 76 | // GetIosSuccess show success counts of iOS notification. 77 | func (s *StateStorage) GetIosSuccess() int64 { 78 | return s.store.Get(core.IosSuccessKey) 79 | } 80 | 81 | // GetIosError show error counts of iOS notification. 82 | func (s *StateStorage) GetIosError() int64 { 83 | return s.store.Get(core.IosErrorKey) 84 | } 85 | 86 | // GetAndroidSuccess show success counts of Android notification. 87 | func (s *StateStorage) GetAndroidSuccess() int64 { 88 | return s.store.Get(core.AndroidSuccessKey) 89 | } 90 | 91 | // GetAndroidError show error counts of Android notification. 92 | func (s *StateStorage) GetAndroidError() int64 { 93 | return s.store.Get(core.AndroidErrorKey) 94 | } 95 | 96 | // GetHuaweiSuccess show success counts of Huawei notification. 97 | func (s *StateStorage) GetHuaweiSuccess() int64 { 98 | return s.store.Get(core.HuaweiSuccessKey) 99 | } 100 | 101 | // GetHuaweiError show error counts of Huawei notification. 102 | func (s *StateStorage) GetHuaweiError() int64 { 103 | return s.store.Get(core.HuaweiErrorKey) 104 | } 105 | -------------------------------------------------------------------------------- /storage/badger/badger.go: -------------------------------------------------------------------------------- 1 | package badger 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | "sync" 8 | 9 | "github.com/appleboy/gorush/core" 10 | 11 | "github.com/dgraph-io/badger/v4" 12 | ) 13 | 14 | var _ core.Storage = (*Storage)(nil) 15 | 16 | // New func implements the storage interface for gorush (https://github.com/appleboy/gorush) 17 | func New(dbPath string) *Storage { 18 | return &Storage{ 19 | dbPath: dbPath, 20 | } 21 | } 22 | 23 | // Storage is interface structure 24 | type Storage struct { 25 | dbPath string 26 | opts badger.Options 27 | name string 28 | db *badger.DB 29 | 30 | sync.RWMutex 31 | } 32 | 33 | func (s *Storage) Add(key string, count int64) { 34 | s.Lock() 35 | defer s.Unlock() 36 | s.setBadger(key, s.getBadger(key)+count) 37 | } 38 | 39 | func (s *Storage) Set(key string, count int64) { 40 | s.Lock() 41 | defer s.Unlock() 42 | s.setBadger(key, count) 43 | } 44 | 45 | func (s *Storage) Get(key string) int64 { 46 | s.RLock() 47 | defer s.RUnlock() 48 | return s.getBadger(key) 49 | } 50 | 51 | // Init client storage. 52 | func (s *Storage) Init() error { 53 | var err error 54 | s.name = "badger" 55 | if s.dbPath == "" { 56 | s.dbPath = os.TempDir() + "badger" 57 | } 58 | s.opts = badger.DefaultOptions(s.dbPath) 59 | 60 | s.db, err = badger.Open(s.opts) 61 | 62 | return err 63 | } 64 | 65 | // Close the storage connection 66 | func (s *Storage) Close() error { 67 | if s.db == nil { 68 | return nil 69 | } 70 | 71 | return s.db.Close() 72 | } 73 | 74 | func (s *Storage) setBadger(key string, count int64) { 75 | err := s.db.Update(func(txn *badger.Txn) error { 76 | value := strconv.FormatInt(count, 10) 77 | return txn.Set([]byte(key), []byte(value)) 78 | }) 79 | if err != nil { 80 | log.Println(s.name, "update error:", err.Error()) 81 | } 82 | } 83 | 84 | func (s *Storage) getBadger(key string) int64 { 85 | var count int64 86 | err := s.db.View(func(txn *badger.Txn) error { 87 | item, err := txn.Get([]byte(key)) 88 | if err != nil { 89 | return err 90 | } 91 | var dst []byte 92 | val, err := item.ValueCopy(dst) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | count, err = strconv.ParseInt(string(val), 10, 64) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return nil 103 | }) 104 | if err != nil { 105 | log.Println(s.name, "get error:", err.Error()) 106 | } 107 | return count 108 | } 109 | -------------------------------------------------------------------------------- /storage/badger/badger_test.go: -------------------------------------------------------------------------------- 1 | package badger 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/appleboy/gorush/core" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestBadgerEngine(t *testing.T) { 13 | var val int64 14 | 15 | badger := New("") 16 | err := badger.Init() 17 | assert.Nil(t, err) 18 | 19 | // reset the value of the key to 0 20 | badger.Set(core.HuaweiSuccessKey, 0) 21 | val = badger.Get(core.HuaweiSuccessKey) 22 | assert.Equal(t, int64(0), val) 23 | 24 | badger.Add(core.HuaweiSuccessKey, 10) 25 | val = badger.Get(core.HuaweiSuccessKey) 26 | assert.Equal(t, int64(10), val) 27 | badger.Add(core.HuaweiSuccessKey, 10) 28 | val = badger.Get(core.HuaweiSuccessKey) 29 | assert.Equal(t, int64(20), val) 30 | 31 | badger.Set(core.HuaweiSuccessKey, 0) 32 | val = badger.Get(core.HuaweiSuccessKey) 33 | assert.Equal(t, int64(0), val) 34 | 35 | // test concurrency issues 36 | var wg sync.WaitGroup 37 | for i := 0; i < 100; i++ { 38 | wg.Add(1) 39 | go func() { 40 | defer wg.Done() 41 | badger.Add(core.HuaweiSuccessKey, 1) 42 | }() 43 | } 44 | wg.Wait() 45 | val = badger.Get(core.HuaweiSuccessKey) 46 | assert.Equal(t, int64(100), val) 47 | 48 | assert.NoError(t, badger.Close()) 49 | } 50 | -------------------------------------------------------------------------------- /storage/boltdb/boltdb.go: -------------------------------------------------------------------------------- 1 | package boltdb 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "sync" 7 | 8 | "github.com/appleboy/gorush/core" 9 | 10 | "github.com/asdine/storm/v3" 11 | ) 12 | 13 | var _ core.Storage = (*Storage)(nil) 14 | 15 | // New func implements the storage interface for gorush (https://github.com/appleboy/gorush) 16 | func New(dbPath, bucket string) *Storage { 17 | return &Storage{ 18 | dbPath: dbPath, 19 | bucket: bucket, 20 | } 21 | } 22 | 23 | // Storage is interface structure 24 | type Storage struct { 25 | dbPath string 26 | bucket string 27 | db *storm.DB 28 | sync.RWMutex 29 | } 30 | 31 | func (s *Storage) Add(key string, count int64) { 32 | s.Lock() 33 | defer s.Unlock() 34 | s.setBoltDB(key, s.getBoltDB(key)+count) 35 | } 36 | 37 | func (s *Storage) Set(key string, count int64) { 38 | s.Lock() 39 | defer s.Unlock() 40 | s.setBoltDB(key, count) 41 | } 42 | 43 | func (s *Storage) Get(key string) int64 { 44 | s.RLock() 45 | defer s.RUnlock() 46 | return s.getBoltDB(key) 47 | } 48 | 49 | // Init client storage. 50 | func (s *Storage) Init() error { 51 | var err error 52 | if s.dbPath == "" { 53 | s.dbPath = os.TempDir() + "boltdb.db" 54 | } 55 | s.db, err = storm.Open(s.dbPath) 56 | return err 57 | } 58 | 59 | // Close the storage connection 60 | func (s *Storage) Close() error { 61 | if s.db == nil { 62 | return nil 63 | } 64 | 65 | return s.db.Close() 66 | } 67 | 68 | func (s *Storage) setBoltDB(key string, count int64) { 69 | err := s.db.Set(s.bucket, key, count) 70 | if err != nil { 71 | log.Println("BoltDB set error:", err.Error()) 72 | } 73 | } 74 | 75 | func (s *Storage) getBoltDB(key string) int64 { 76 | var count int64 77 | err := s.db.Get(s.bucket, key, &count) 78 | if err != nil { 79 | log.Println("BoltDB get error:", err.Error()) 80 | } 81 | return count 82 | } 83 | -------------------------------------------------------------------------------- /storage/boltdb/boltdb_test.go: -------------------------------------------------------------------------------- 1 | package boltdb 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/appleboy/gorush/core" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestBoltDBEngine(t *testing.T) { 13 | var val int64 14 | 15 | boltDB := New("", "gorush") 16 | err := boltDB.Init() 17 | assert.Nil(t, err) 18 | 19 | // reset the value of the key to 0 20 | boltDB.Set(core.HuaweiSuccessKey, 0) 21 | val = boltDB.Get(core.HuaweiSuccessKey) 22 | assert.Equal(t, int64(0), val) 23 | 24 | boltDB.Add(core.HuaweiSuccessKey, 10) 25 | val = boltDB.Get(core.HuaweiSuccessKey) 26 | assert.Equal(t, int64(10), val) 27 | boltDB.Add(core.HuaweiSuccessKey, 10) 28 | val = boltDB.Get(core.HuaweiSuccessKey) 29 | assert.Equal(t, int64(20), val) 30 | 31 | boltDB.Set(core.HuaweiSuccessKey, 0) 32 | val = boltDB.Get(core.HuaweiSuccessKey) 33 | assert.Equal(t, int64(0), val) 34 | 35 | // test concurrency issues 36 | var wg sync.WaitGroup 37 | for i := 0; i < 10; i++ { 38 | wg.Add(1) 39 | go func() { 40 | boltDB.Add(core.HuaweiSuccessKey, 1) 41 | wg.Done() 42 | }() 43 | } 44 | wg.Wait() 45 | val = boltDB.Get(core.HuaweiSuccessKey) 46 | assert.Equal(t, int64(10), val) 47 | 48 | assert.NoError(t, boltDB.Close()) 49 | } 50 | -------------------------------------------------------------------------------- /storage/buntdb/buntdb.go: -------------------------------------------------------------------------------- 1 | package buntdb 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strconv" 8 | "sync" 9 | 10 | "github.com/appleboy/gorush/core" 11 | 12 | "github.com/tidwall/buntdb" 13 | ) 14 | 15 | var _ core.Storage = (*Storage)(nil) 16 | 17 | // New func implements the storage interface for gorush (https://github.com/appleboy/gorush) 18 | func New(dbPath string) *Storage { 19 | return &Storage{ 20 | dbPath: dbPath, 21 | } 22 | } 23 | 24 | // Storage is interface structure 25 | type Storage struct { 26 | dbPath string 27 | db *buntdb.DB 28 | sync.RWMutex 29 | } 30 | 31 | func (s *Storage) Add(key string, count int64) { 32 | s.Lock() 33 | defer s.Unlock() 34 | s.setBuntDB(key, s.getBuntDB(key)+count) 35 | } 36 | 37 | func (s *Storage) Set(key string, count int64) { 38 | s.Lock() 39 | defer s.Unlock() 40 | s.setBuntDB(key, count) 41 | } 42 | 43 | func (s *Storage) Get(key string) int64 { 44 | s.RLock() 45 | defer s.RUnlock() 46 | return s.getBuntDB(key) 47 | } 48 | 49 | // Init client storage. 50 | func (s *Storage) Init() error { 51 | var err error 52 | if s.dbPath == "" { 53 | s.dbPath = os.TempDir() + "buntdb.db" 54 | } 55 | s.db, err = buntdb.Open(s.dbPath) 56 | return err 57 | } 58 | 59 | // Close the storage connection 60 | func (s *Storage) Close() error { 61 | if s.db == nil { 62 | return nil 63 | } 64 | 65 | return s.db.Close() 66 | } 67 | 68 | func (s *Storage) setBuntDB(key string, count int64) { 69 | err := s.db.Update(func(tx *buntdb.Tx) error { 70 | if _, _, err := tx.Set(key, fmt.Sprintf("%d", count), nil); err != nil { 71 | return err 72 | } 73 | return nil 74 | }) 75 | if err != nil { 76 | log.Println("BuntDB update error:", err.Error()) 77 | } 78 | } 79 | 80 | func (s *Storage) getBuntDB(key string) int64 { 81 | var count int64 82 | err := s.db.View(func(tx *buntdb.Tx) error { 83 | val, _ := tx.Get(key) 84 | count, _ = strconv.ParseInt(val, 10, 64) 85 | return nil 86 | }) 87 | if err != nil { 88 | log.Println("BuntDB get error:", err.Error()) 89 | } 90 | 91 | return count 92 | } 93 | -------------------------------------------------------------------------------- /storage/buntdb/buntdb_test.go: -------------------------------------------------------------------------------- 1 | package buntdb 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/appleboy/gorush/core" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestBuntDBEngine(t *testing.T) { 13 | var val int64 14 | 15 | buntDB := New("") 16 | err := buntDB.Init() 17 | assert.Nil(t, err) 18 | 19 | // reset the value of the key to 0 20 | buntDB.Set(core.HuaweiSuccessKey, 0) 21 | val = buntDB.Get(core.HuaweiSuccessKey) 22 | assert.Equal(t, int64(0), val) 23 | 24 | buntDB.Add(core.HuaweiSuccessKey, 10) 25 | val = buntDB.Get(core.HuaweiSuccessKey) 26 | assert.Equal(t, int64(10), val) 27 | buntDB.Add(core.HuaweiSuccessKey, 10) 28 | val = buntDB.Get(core.HuaweiSuccessKey) 29 | assert.Equal(t, int64(20), val) 30 | 31 | buntDB.Set(core.HuaweiSuccessKey, 0) 32 | val = buntDB.Get(core.HuaweiSuccessKey) 33 | assert.Equal(t, int64(0), val) 34 | 35 | // test concurrency issues 36 | var wg sync.WaitGroup 37 | for i := 0; i < 10; i++ { 38 | wg.Add(1) 39 | go func() { 40 | buntDB.Add(core.HuaweiSuccessKey, 1) 41 | wg.Done() 42 | }() 43 | } 44 | wg.Wait() 45 | val = buntDB.Get(core.HuaweiSuccessKey) 46 | assert.Equal(t, int64(10), val) 47 | 48 | assert.NoError(t, buntDB.Close()) 49 | } 50 | -------------------------------------------------------------------------------- /storage/leveldb/leveldb.go: -------------------------------------------------------------------------------- 1 | package leveldb 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "sync" 8 | 9 | "github.com/appleboy/gorush/core" 10 | 11 | "github.com/syndtr/goleveldb/leveldb" 12 | ) 13 | 14 | var _ core.Storage = (*Storage)(nil) 15 | 16 | func (s *Storage) setLevelDB(key string, count int64) { 17 | value := fmt.Sprintf("%d", count) 18 | _ = s.db.Put([]byte(key), []byte(value), nil) 19 | } 20 | 21 | func (s *Storage) getLevelDB(key string) int64 { 22 | data, _ := s.db.Get([]byte(key), nil) 23 | count, _ := strconv.ParseInt(string(data), 10, 64) 24 | return count 25 | } 26 | 27 | // New func implements the storage interface for gorush (https://github.com/appleboy/gorush) 28 | func New(dbPath string) *Storage { 29 | return &Storage{ 30 | dbPath: dbPath, 31 | } 32 | } 33 | 34 | // Storage is interface structure 35 | type Storage struct { 36 | dbPath string 37 | db *leveldb.DB 38 | sync.RWMutex 39 | } 40 | 41 | func (s *Storage) Add(key string, count int64) { 42 | s.Lock() 43 | defer s.Unlock() 44 | s.setLevelDB(key, s.getLevelDB(key)+count) 45 | } 46 | 47 | func (s *Storage) Set(key string, count int64) { 48 | s.Lock() 49 | defer s.Unlock() 50 | s.setLevelDB(key, count) 51 | } 52 | 53 | func (s *Storage) Get(key string) int64 { 54 | s.RLock() 55 | defer s.RUnlock() 56 | return s.getLevelDB(key) 57 | } 58 | 59 | // Init client storage. 60 | func (s *Storage) Init() error { 61 | var err error 62 | if s.dbPath == "" { 63 | s.dbPath = os.TempDir() + "leveldb.db" 64 | } 65 | s.db, err = leveldb.OpenFile(s.dbPath, nil) 66 | return err 67 | } 68 | 69 | // Close the storage connection 70 | func (s *Storage) Close() error { 71 | if s.db == nil { 72 | return nil 73 | } 74 | 75 | return s.db.Close() 76 | } 77 | -------------------------------------------------------------------------------- /storage/leveldb/leveldb_test.go: -------------------------------------------------------------------------------- 1 | package leveldb 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/appleboy/gorush/core" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestLevelDBEngine(t *testing.T) { 13 | var val int64 14 | 15 | levelDB := New("") 16 | err := levelDB.Init() 17 | assert.Nil(t, err) 18 | 19 | // reset the value of the key to 0 20 | levelDB.Set(core.HuaweiSuccessKey, 0) 21 | val = levelDB.Get(core.HuaweiSuccessKey) 22 | assert.Equal(t, int64(0), val) 23 | 24 | levelDB.Add(core.HuaweiSuccessKey, 10) 25 | val = levelDB.Get(core.HuaweiSuccessKey) 26 | assert.Equal(t, int64(10), val) 27 | levelDB.Add(core.HuaweiSuccessKey, 10) 28 | val = levelDB.Get(core.HuaweiSuccessKey) 29 | assert.Equal(t, int64(20), val) 30 | 31 | levelDB.Set(core.HuaweiSuccessKey, 0) 32 | val = levelDB.Get(core.HuaweiSuccessKey) 33 | assert.Equal(t, int64(0), val) 34 | 35 | // test concurrency issues 36 | var wg sync.WaitGroup 37 | for i := 0; i < 10; i++ { 38 | wg.Add(1) 39 | go func() { 40 | levelDB.Add(core.HuaweiSuccessKey, 1) 41 | wg.Done() 42 | }() 43 | } 44 | wg.Wait() 45 | val = levelDB.Get(core.HuaweiSuccessKey) 46 | assert.Equal(t, int64(10), val) 47 | 48 | assert.NoError(t, levelDB.Close()) 49 | } 50 | -------------------------------------------------------------------------------- /storage/memory/memory.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/appleboy/gorush/core" 7 | 8 | "go.uber.org/atomic" 9 | ) 10 | 11 | var _ core.Storage = (*Storage)(nil) 12 | 13 | // New func implements the storage interface for gorush (https://github.com/appleboy/gorush) 14 | func New() *Storage { 15 | return &Storage{} 16 | } 17 | 18 | // Storage is interface structure 19 | type Storage struct { 20 | mem sync.Map 21 | } 22 | 23 | func (s *Storage) getValueBtKey(key string) *atomic.Int64 { 24 | if val, ok := s.mem.Load(key); ok { 25 | return val.(*atomic.Int64) 26 | } 27 | val := atomic.NewInt64(0) 28 | s.mem.Store(key, val) 29 | return val 30 | } 31 | 32 | func (s *Storage) Add(key string, count int64) { 33 | s.getValueBtKey(key).Add(count) 34 | } 35 | 36 | func (s *Storage) Set(key string, count int64) { 37 | s.getValueBtKey(key).Store(count) 38 | } 39 | 40 | func (s *Storage) Get(key string) int64 { 41 | return s.getValueBtKey(key).Load() 42 | } 43 | 44 | // Init client storage. 45 | func (*Storage) Init() error { 46 | return nil 47 | } 48 | 49 | // Close the storage connection 50 | func (*Storage) Close() error { 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /storage/memory/memory_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/appleboy/gorush/core" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestMemoryEngine(t *testing.T) { 13 | var val int64 14 | 15 | memory := New() 16 | err := memory.Init() 17 | assert.Nil(t, err) 18 | 19 | // reset the value of the key to 0 20 | memory.Set(core.HuaweiSuccessKey, 0) 21 | val = memory.Get(core.HuaweiSuccessKey) 22 | assert.Equal(t, int64(0), val) 23 | 24 | memory.Add(core.HuaweiSuccessKey, 10) 25 | val = memory.Get(core.HuaweiSuccessKey) 26 | assert.Equal(t, int64(10), val) 27 | memory.Add(core.HuaweiSuccessKey, 10) 28 | val = memory.Get(core.HuaweiSuccessKey) 29 | assert.Equal(t, int64(20), val) 30 | 31 | memory.Set(core.HuaweiSuccessKey, 0) 32 | val = memory.Get(core.HuaweiSuccessKey) 33 | assert.Equal(t, int64(0), val) 34 | 35 | // test concurrency issues 36 | var wg sync.WaitGroup 37 | for i := 0; i < 10; i++ { 38 | wg.Add(1) 39 | go func() { 40 | memory.Add(core.HuaweiSuccessKey, 1) 41 | wg.Done() 42 | }() 43 | } 44 | wg.Wait() 45 | val = memory.Get(core.HuaweiSuccessKey) 46 | assert.Equal(t, int64(10), val) 47 | 48 | assert.NoError(t, memory.Close()) 49 | } 50 | -------------------------------------------------------------------------------- /storage/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/appleboy/gorush/core" 11 | 12 | "github.com/redis/go-redis/v9" 13 | ) 14 | 15 | var _ core.Storage = (*Storage)(nil) 16 | 17 | // New func implements the storage interface for gorush (https://github.com/appleboy/gorush) 18 | func New( 19 | addr string, 20 | username string, 21 | password string, 22 | db int, 23 | isCluster bool, 24 | ) *Storage { 25 | return &Storage{ 26 | ctx: context.Background(), 27 | addr: addr, 28 | username: username, 29 | password: password, 30 | db: db, 31 | isCluster: isCluster, 32 | } 33 | } 34 | 35 | // Storage is interface structure 36 | type Storage struct { 37 | ctx context.Context 38 | client redis.Cmdable 39 | addr string 40 | username string 41 | password string 42 | db int 43 | isCluster bool 44 | } 45 | 46 | func (s *Storage) Add(key string, count int64) { 47 | s.client.IncrBy(s.ctx, key, count) 48 | } 49 | 50 | func (s *Storage) Set(key string, count int64) { 51 | s.client.Set(s.ctx, key, count, 0) 52 | } 53 | 54 | func (s *Storage) Get(key string) int64 { 55 | val, _ := s.client.Get(s.ctx, key).Result() 56 | count, _ := strconv.ParseInt(val, 10, 64) 57 | return count 58 | } 59 | 60 | // Init client storage. 61 | func (s *Storage) Init() error { 62 | if s.isCluster { 63 | s.client = redis.NewClusterClient(&redis.ClusterOptions{ 64 | Addrs: strings.Split(s.addr, ","), 65 | Username: s.username, 66 | Password: s.password, 67 | }) 68 | } else { 69 | s.client = redis.NewClient(&redis.Options{ 70 | Addr: s.addr, 71 | Password: s.password, 72 | DB: s.db, 73 | }) 74 | } 75 | 76 | if err := s.client.Ping(s.ctx).Err(); err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // Close the storage connection 84 | func (s *Storage) Close() error { 85 | switch v := s.client.(type) { 86 | case *redis.Client: 87 | return v.Close() 88 | case *redis.ClusterClient: 89 | return v.Close() 90 | case nil: 91 | return nil 92 | default: 93 | // this will not happen anyway, unless we mishandle it on `Init` 94 | panic(fmt.Sprintf("invalid redis client: %v", reflect.TypeOf(v))) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /storage/redis/redis_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/appleboy/gorush/core" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRedisServerError(t *testing.T) { 13 | redis := New( 14 | "redis:6370", // addr 15 | "", // username 16 | "", // password 17 | 0, // db 18 | false, // cluster 19 | ) 20 | err := redis.Init() 21 | 22 | assert.Error(t, err) 23 | } 24 | 25 | func TestRedisEngine(t *testing.T) { 26 | var val int64 27 | 28 | redis := New( 29 | "redis:6379", // addr 30 | "", // username 31 | "", // password 32 | 0, // db 33 | false, // cluster 34 | ) 35 | err := redis.Init() 36 | assert.Nil(t, err) 37 | 38 | // reset the value of the key to 0 39 | redis.Set(core.HuaweiSuccessKey, 0) 40 | val = redis.Get(core.HuaweiSuccessKey) 41 | assert.Equal(t, int64(0), val) 42 | 43 | redis.Add(core.HuaweiSuccessKey, 10) 44 | val = redis.Get(core.HuaweiSuccessKey) 45 | assert.Equal(t, int64(10), val) 46 | redis.Add(core.HuaweiSuccessKey, 10) 47 | val = redis.Get(core.HuaweiSuccessKey) 48 | assert.Equal(t, int64(20), val) 49 | 50 | redis.Set(core.HuaweiSuccessKey, 0) 51 | val = redis.Get(core.HuaweiSuccessKey) 52 | assert.Equal(t, int64(0), val) 53 | 54 | // test concurrency issues 55 | var wg sync.WaitGroup 56 | for i := 0; i < 10; i++ { 57 | wg.Add(1) 58 | go func() { 59 | redis.Add(core.HuaweiSuccessKey, 1) 60 | wg.Done() 61 | }() 62 | } 63 | wg.Wait() 64 | val = redis.Get(core.HuaweiSuccessKey) 65 | assert.Equal(t, int64(10), val) 66 | 67 | assert.NoError(t, redis.Close()) 68 | } 69 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | How to test gorush with http request? 4 | 5 | ## download bat tool 6 | 7 | Download [cURL-like tool for humans](https://github.com/astaxie/bat). 8 | 9 | ## testing 10 | 11 | see the JSON format: 12 | 13 | ```json 14 | { 15 | "notifications": [ 16 | { 17 | "tokens": ["token_a", "token_b"], 18 | "platform": 1, 19 | "message": "Hello World iOS!" 20 | }, 21 | { 22 | "tokens": ["token_a", "token_b"], 23 | "platform": 2, 24 | "message": "Hello World Android!" 25 | } 26 | ] 27 | } 28 | ``` 29 | 30 | run the following command. 31 | 32 | ```sh 33 | bat POST localhost:8088/api/push < tests/test.json 34 | ``` 35 | 36 | Here is a sample shell code to calculate factorial using while loop: 37 | 38 | ```sh 39 | #!/bin/bash 40 | counter=$1 41 | while [ $counter -gt 0 ] 42 | do 43 | bat POST https://gorush.netlify.app/api/push < tests/test.json 44 | counter=$(( $counter - 1 )) 45 | echo $counter 46 | done 47 | ``` 48 | -------------------------------------------------------------------------------- /tests/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "notifications": [ 3 | { 4 | "tokens": ["token_a", "token_b"], 5 | "platform": 1, 6 | "message": "Hello World iOS!", 7 | "title": "Gorush with iOS" 8 | }, 9 | { 10 | "tokens": ["token_a", "token_b"], 11 | "platform": 2, 12 | "message": "Hello World Android!", 13 | "title": "Gorush with Android" 14 | }, 15 | { 16 | "tokens": ["token_a", "token_b"], 17 | "platform": 3, 18 | "message": "Hello World Huawei!", 19 | "title": "Gorush with HMS" 20 | } 21 | ] 22 | } 23 | --------------------------------------------------------------------------------