diff --git a/README.md b/README.md index 9e1c5102f..bf58f87ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # gorush -A push notification micro server using [Gin](https://github.com/gin-gonic/gin) framework written in Go (Golang) and see the [demo app](https://github.com/appleboy/flutter-gorush). +A push notification micro server using [Gin](https://github.com/gin-gonic/gin) framework written in Go (Golang) and see +the [demo app](https://github.com/appleboy/flutter-gorush). [![Run Lint and Testing](https://github.com/appleboy/gorush/actions/workflows/lint.yml/badge.svg)](https://github.com/appleboy/gorush/actions/workflows/lint.yml) [![GoDoc](https://godoc.org/github.com/appleboy/gorush?status.svg)](https://pkg.go.dev/github.com/appleboy/gorush) @@ -68,9 +69,12 @@ A push notification micro server using [Gin](https://github.com/gin-gonic/gin) f ## Features -- Support [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) using [go-fcm](https://github.com/appleboy/go-fcm) library for Android. -- Support [HTTP/2](https://http2.github.io/) Apple Push Notification Service using [apns2](https://github.com/sideshow/apns2) library. -- Support [HMS Push Service](https://developer.huawei.com/consumer/en/hms/huawei-pushkit) using [go-hms-push](https://github.com/msalihkarakasli/go-hms-push) library for Huawei Devices. +- Support [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) + using [go-fcm](https://github.com/appleboy/go-fcm) library for Android. +- Support [HTTP/2](https://http2.github.io/) Apple Push Notification Service + using [apns2](https://github.com/sideshow/apns2) library. +- Support [HMS Push Service](https://developer.huawei.com/consumer/en/hms/huawei-pushkit) + using [go-hms-push](https://github.com/msalihkarakasli/go-hms-push) library for Huawei Devices. - Support [YAML](https://github.com/go-yaml/yaml) configuration. - Support command line to send single Android or iOS notification. - Support Web API to send push notification. @@ -78,15 +82,19 @@ A push notification micro server using [Gin](https://github.com/gin-gonic/gin) f - Support notification queue and multiple workers. - Support `/api/stat/app` show notification success and failure counts. - Support `/api/config` show your [YAML](https://en.wikipedia.org/wiki/YAML) config. -- Support store app stat to memory, [Redis](http://redis.io/), [BoltDB](https://github.com/boltdb/bolt), [BuntDB](https://github.com/tidwall/buntdb), [LevelDB](https://github.com/syndtr/goleveldb) or [BadgerDB](https://github.com/dgraph-io/badger). +- Support store app stat to memory, [Redis](http://redis.io/), [BoltDB](https://github.com/boltdb/bolt) + , [BuntDB](https://github.com/tidwall/buntdb), [LevelDB](https://github.com/syndtr/goleveldb) + or [BadgerDB](https://github.com/dgraph-io/badger). - Support `p8`, `p12` or `pem` format of iOS certificate file. - Support `/sys/stats` show response time, status code count, etc. - Support for HTTP, HTTPS or SOCKS5 proxy. - Support retry send notification if server response is fail. - Support expose [prometheus](https://prometheus.io/) metrics. - Support install TLS certificates from [Let's Encrypt](https://letsencrypt.org/) automatically. -- Support send notification through [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) protocol, we use [gRPC](https://grpc.io/) as default framework. -- Support running in Docker, [Kubernetes](https://kubernetes.io/) or [AWS Lambda](https://aws.amazon.com/lambda) ([Native Support in Golang](https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/)) +- Support send notification through [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) protocol, we + use [gRPC](https://grpc.io/) as default framework. +- Support running in Docker, [Kubernetes](https://kubernetes.io/) + or [AWS Lambda](https://aws.amazon.com/lambda) ([Native Support in Golang](https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/)) - Support graceful shutdown that workers and queue have been sent to APNs/FCM before shutdown service. - Support different Queue as backend like [NSQ](https://nsq.io/), [NATS](https://nats.io/) or [Redis streams](https://redis.io/docs/manual/data-types/streams/), defaut engine is local [Channel](https://tour.golang.org/concurrency/2). @@ -124,27 +132,20 @@ grpc: enabled: false # enable gRPC server port: 9000 -api: - push_uri: "/api/push" - stat_go_uri: "/api/stat/go" - stat_app_uri: "/api/stat/app" - config_uri: "/api/config" - sys_stat_uri: "/sys/stats" - metric_uri: "/metrics" - health_uri: "/healthz" - -android: - enabled: true - apikey: "YOUR_API_KEY" - max_retry: 0 # resend fail notification, default value zero is disabled +tenants: + - tenant_id1: + push_uri: "/api/push/tenant1" # must be in the form of /api/push/:tenant_id + android: + enabled: true + max_retry: 0 # resend fail notification, default value zero is disabled -huawei: - enabled: false - appsecret: "YOUR_APP_SECRET" - appid: "YOUR_APP_ID" - max_retry: 0 # resend fail notification, default value zero is disabled + huawei: + enabled: true + api_key: "YOUR_API_KEY" + app_id: "YOUR_APP_ID" + max_retry: 0 # resend fail notification, default value zero is disabled -queue: + queue: engine: "local" # support "local", "nsq", "nats" and "redis" default value is "local" nsq: addr: 127.0.0.1:4150 @@ -158,19 +159,25 @@ queue: addr: 127.0.0.1:6379 group: gorush consumer: gorush - stream_name: gorush + stream_name: gorushios: + enabled: false + key_path: "key.pem" + key_base64: "" # load iOS key from base64 input + key_type: "pem" # could be pem, p12 or p8 type + password: "" # certificate password, default as empty string. + production: false + max_concurrent_pushes: 100 # just for push ios notification + max_retry: 0 # resend fail notification, default value zero is disabled + key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) + team_id: "" # TeamID from developer account (View Account -> Membership) -ios: - enabled: false - key_path: "key.pem" - key_base64: "" # load iOS key from base64 input - key_type: "pem" # could be pem, p12 or p8 type - password: "" # certificate password, default as empty string. - production: false - max_concurrent_pushes: 100 # just for push ios notification - max_retry: 0 # resend fail notification, default value zero is disabled - key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) - team_id: "" # TeamID from developer account (View Account -> Membership) +api: + stat_go_uri: "/api/stat/go" + stat_app_uri: "/api/stat/app" + config_uri: "/api/config" + sys_stat_uri: "/sys/stats" + metric_uri: "/metrics" + health_uri: "/healthz" log: format: "string" # string or json @@ -208,7 +215,7 @@ Memory average usage: **28Mb** (the total bytes of memory obtained from the OS.) Test Command (We use [bat](https://github.com/astaxie/bat) as default cli tool.): ```sh -for i in {1..9999999}; do bat -b.N=1000 -b.C=100 POST localhost:8088/api/push notifications:=@notification.json; sleep 1; done +for i in {1..9999999}; do bat -b.N=1000 -b.C=100 POST localhost:8088/api/push/1 notifications:=@notification.json; sleep 1; done ``` ## Basic Usage @@ -252,7 +259,8 @@ wget -c https://github.com/appleboy/gorush/releases/download/v1.16.1/gorush_1.16 #### Fetch from GitHub -Gorush uses the Go Modules support built into Go 1.11 to build. The easiest way to get started is to clone Gorush in a directory outside of the GOPATH, as in the following example: +Gorush uses the Go Modules support built into Go 1.15 to build. The easiest way to get started is to clone Gorush in a +directory outside of the GOPATH, as in the following example: ```sh mkdir $HOME/src @@ -313,6 +321,7 @@ Common Options: --topic iOS or Android topic message -h, --help Show this message -V, --version Show version + -tid, --tenant Sets the tenant id ``` ### Send Android notification @@ -320,7 +329,7 @@ Common Options: Send single notification with the following command. ```bash -gorush -android -m "your message" -k "API Key" -t "Device token" +gorush -android -m "your message" -k "API Key" -t "Device token" -tid "tenant" ``` Send messages to topics. @@ -328,12 +337,14 @@ Send messages to topics. ```bash gorush --android --topic "/topics/foo-bar" \ -m "This is a Firebase Cloud Messaging Topic Message" \ - -k your_api_key + -k your_api_key \ + -tid "tenant" ``` - `-m`: Notification message. - `-k`: [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) api key - `-t`: Device token. +- `-tid`: Tenant ID. - `--title`: Notification title. - `--topic`: Send messages to topics. note: don't add device token. - `--proxy`: Set `http`, `https` or `socks5` proxy url. @@ -343,7 +354,7 @@ gorush --android --topic "/topics/foo-bar" \ Send single notification with the following command. ```bash -gorush -huawei -title "Gorush with HMS" -m "your message" -hk "API Key" -hid "App ID" -t "Device token" +gorush -huawei -title "Gorush with HMS" -m "your message" -hk "API Key" -hid "App ID" -t "Device token" -tid "tenant" ``` Send messages to topics. @@ -353,12 +364,14 @@ gorush --huawei --topic "foo-bar" \ -title "Gorush with HMS" \ -m "This is a Huawei Mobile Services Topic Message" \ -hk "API Key" \ - -hid "App ID" + -hid "App ID" \ + -tid "tenant" ``` - `-m`: Notification message. - `-hk`: [Huawei Mobile Services](https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/Preparations) api secret key - `-t`: Device token. +- `-tid`: Tenant ID. - `--title`: Notification title. - `--topic`: Send messages to topics. note: don't add device token. - `--proxy`: Set `http`, `https` or `socks5` proxy url. @@ -369,7 +382,7 @@ Send single notification with the following command. ```bash $ gorush -ios -m "your message" -i "your certificate path" \ - -t "device token" --topic "apns topic" + -t "device token" --topic "apns topic" --tenantid "tenantId" ``` - `-m`: Notification message. @@ -378,13 +391,15 @@ $ gorush -ios -m "your message" -i "your certificate path" \ - `--title`: Notification title. - `--topic`: The topic of the remote notification. - `--password`: The certificate password. +- `--tenandid`: The tenant ID. The default endpoint is APNs development. Please add `-production` flag for APNs production push endpoint. ```bash $ gorush -ios -m "your message" -i "your certificate path" \ -t "device token" \ - -production + -production \ + -tid "tenandId" ``` ### Send Android or iOS notifications using Firebase @@ -392,7 +407,7 @@ $ gorush -ios -m "your message" -i "your certificate path" \ Send single notification with the following command: ```bash -gorush -android -m "your message" -k "API key" -t "Device token" +gorush -android -m "your message" -k "API key" -t "Device token" -tid "tenantId" ``` ## Run gorush web server @@ -416,10 +431,11 @@ http -v --verify=no --json GET http://localhost:8088/api/stat/go Gorush support the following API. -- **GET** `/api/stat/go` Golang cpu, memory, gc, etc information. Thanks for [golang-stats-api-handler](https://github.com/fukata/golang-stats-api-handler). +- **GET** `/api/stat/go` Golang cpu, memory, gc, etc information. Thanks + for [golang-stats-api-handler](https://github.com/fukata/golang-stats-api-handler). - **GET** `/api/stat/app` show notification success and failure counts. - **GET** `/api/config` show server yml config file. -- **POST** `/api/push` push ios, android or huawei notifications. +- **POST** `/api/push/:tenant_push_uri` push ios, android or huawei notifications. ### GET /api/stat/go @@ -498,7 +514,7 @@ Show response time, status code count, etc. "uptime_sec": 102.428010614, "time": "2016-06-26 12:27:11.675973571 +0800 CST", "unixtime": 1466915231, - "status_code_count": { }, + "status_code_count": {}, "total_status_code_count": { "200": 5 }, @@ -525,7 +541,10 @@ Simple send iOS notification example, the `platform` value is `1`: { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 1, "message": "Hello World iOS!" } @@ -539,7 +558,10 @@ Simple send Android notification example, the `platform` value is `2`: { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 2, "message": "Hello World Android!" } @@ -553,7 +575,10 @@ Simple send Huawei notification example, the `platform` value is `3`: { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 3, "title": "Gorush with HMS", "message": "Hello World Huawei!" @@ -568,7 +593,10 @@ Simple send notification on Android and iOS devices using Firebase, the `platfor { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 2, "message": "This notification will go to iOS and Android platform via Firebase!" } @@ -582,22 +610,30 @@ Send multiple notifications as below: { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 1, "message": "Hello World iOS!" }, { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 2, "message": "Hello World Android!" }, { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 3, "message": "Hello World Huawei!", "title": "Gorush with HMS" - }, - ..... + } ] } ``` @@ -664,7 +700,9 @@ The Request body must have a notifications array. The following is a parameter t | title-loc-args | array of strings | Variable string values to appear in place of the format specifiers in title-loc-key. | - | | | title-loc-key | string | The key to a title string in the Localizable.strings file for the current localization. | - | | -See more detail about [APNs Remote Notification Payload](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html). +See more detail +about [APNs Remote Notification Payload](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) +. ### iOS sound payload @@ -699,7 +737,9 @@ request format: | title_loc_key | string | Indicates the key to the title string for localization. | - | | | title_loc_args | string | Indicates the string value to replace format specifiers in title string for localization. | - | | -See more detail about [Firebase Cloud Messaging HTTP Protocol reference](https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream). +See more detail +about [Firebase Cloud Messaging HTTP Protocol reference](https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream) +. ### Huawei notification @@ -711,7 +751,9 @@ See more detail about [Firebase Cloud Messaging HTTP Protocol reference](https:/ 6. huawei_ttl: mapped to ttl 7. huawei_collapse_key: mapped to collapse_key -See more detail about [Huawei Mobulse Services Push API reference](https://developer.huawei.com/consumer/en/doc/development/HMS-References/push-sendapi). +See more detail +about [Huawei Mobulse Services Push API reference](https://developer.huawei.com/consumer/en/doc/development/HMS-References/push-sendapi) +. ### iOS Example @@ -721,7 +763,10 @@ Send normal notification. { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 1, "message": "Hello World iOS!" } @@ -729,32 +774,42 @@ Send normal notification. } ``` -The following payload asks the system to display an alert with a Close button and a single action button.The title and body keys provide the contents of the alert. The “PLAY” string is used to retrieve a localized string from the appropriate Localizable.strings file of the app. The resulting string is used by the alert as the title of an action button. This payload also asks the system to badge the app’s icon with the number 5. +The following payload asks the system to display an alert with a Close button and a single action button.The title and +body keys provide the contents of the alert. The “PLAY” string is used to retrieve a localized string from the +appropriate Localizable.strings file of the app. The resulting string is used by the alert as the title of an action +button. This payload also asks the system to badge the app’s icon with the number 5. ```json { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 1, "badge": 5, "alert": { - "title" : "Game Request", - "body" : "Bob wants to play poker", - "action-loc-key" : "PLAY" + "title": "Game Request", + "body": "Bob wants to play poker", + "action-loc-key": "PLAY" } } ] } ``` -The following payload specifies that the device should display an alert message, plays a sound, and badges the app’s icon. +The following payload specifies that the device should display an alert message, plays a sound, and badges the app’s +icon. ```json { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 1, "message": "You got your emails.", "badge": 9, @@ -774,7 +829,10 @@ Add other fields which user defined via `data` field. { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 1, "message": "Hello World iOS!", "data": { @@ -786,7 +844,8 @@ Add other fields which user defined via `data` field. } ``` -Support send notification from different environment. See the detail of [issue](https://github.com/appleboy/gorush/issues/246). +Support send notification from different environment. See the detail +of [issue](https://github.com/appleboy/gorush/issues/246). ```diff { @@ -815,7 +874,10 @@ Send normal notification. { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 2, "message": "Hello World Android!", "title": "You got message" @@ -830,11 +892,14 @@ Add `notification` payload. { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 2, "message": "Hello World Android!", "title": "You got message", - "notification" : { + "notification": { "icon": "myicon", "color": "#112244" } @@ -849,14 +914,17 @@ Add other fields which user defined via `data` field. { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 2, "message": "Hello World Android!", "title": "You got message", "data": { - "Nick" : "Mario", - "body" : "great match!", - "Room" : "PortugalVSDenmark" + "Nick": "Mario", + "body": "great match!", + "Room": "PortugalVSDenmark" } } ] @@ -885,7 +953,10 @@ Send normal notification. { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 3, "message": "Hello World Huawei!", "title": "You got message" @@ -900,11 +971,14 @@ Add `notification` payload. { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 3, "message": "Hello World Huawei!", "title": "You got message", - "huawei_notification" : { + "huawei_notification": { "icon": "myicon", "color": "#112244" } @@ -919,7 +993,10 @@ Add other fields which user defined via `huawei_data` field. { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 3, "huawei_data": "{'title' : 'Mario','message' : 'great match!', 'Room' : 'PortugalVSDenmark'}" } @@ -962,7 +1039,8 @@ Success response: } ``` -If you need error logs from sending fail notifications, please set a `feedback_hook_url`. The server with send the failing logs asynchronously to your API as `POST` requests. +If you need error logs from sending fail notifications, please set a `feedback_hook_url`. The server with send the +failing logs asynchronously to your API as `POST` requests. ```diff core: @@ -1021,7 +1099,8 @@ See the following error format. ## Run gRPC service -Gorush support [gRPC](https://grpc.io/) service. You can enable the gRPC in `config.yml`, default as disabled. Enable the gRPC server: +Gorush support [gRPC](https://grpc.io/) service. You can enable the gRPC in `config.yml`, default as disabled. Enable +the gRPC server: ```sh GORUSH_GRPC_ENABLED=true GORUSH_GRPC_PORT=3000 gorush @@ -1056,6 +1135,7 @@ func main() { c := proto.NewGorushClient(conn) r, err := c.Send(context.Background(), &proto.NotificationRequest{ + TenantId: "tenantId", Platform: 2, Tokens: []string{"1234567890"}, Message: "test message", @@ -1083,12 +1163,9 @@ func main() { }) if err != nil { log.Println("could not greet: ", err) - } - - if r != nil { - log.Printf("Success: %t\n", r.Success) - log.Printf("Count: %d\n", r.Counts) - } + }if r != nil { + log.Printf("Success: %t\n", r.Success) + log.Printf("Count: %d\n", r.Counts)} } ``` @@ -1118,7 +1195,7 @@ function main() { request.setContentavailable(false); request.setMutablecontent(false); client.send(request, function (err, response) { - if(err) { + if (err) { console.log(err); } else { console.log("Success:", response.getSuccess()); @@ -1186,18 +1263,16 @@ func main() { }) if err != nil { log.Println("could not greet: ", err) - } - - if r != nil { - log.Printf("Success: %t\n", r.Success) - log.Printf("Count: %d\n", r.Counts) - } + }if r != nil { + log.Printf("Success: %t\n", r.Success) + log.Printf("Count: %d\n", r.Counts)} } ``` ## Run gorush in Docker -Set up `gorush` in the cloud in under 5 minutes with zero knowledge of Golang or Linux shell using our [gorush Docker image](https://hub.docker.com/r/appleboy/gorush/). +Set up `gorush` in the cloud in under 5 minutes with zero knowledge of Golang or Linux shell using +our [gorush Docker image](https://hub.docker.com/r/appleboy/gorush/). ```bash docker pull appleboy/gorush @@ -1277,7 +1352,10 @@ kubectl delete -f k8s ![lambda](./screenshot/lambda.png) -AWS excited to [announce Go as a supported language for AWS Lambda](https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/). You’re going to create an application that uses an [API Gateway](https://aws.amazon.com/apigateway) event source to create a simple Hello World RESTful API. +AWS excited +to [announce Go as a supported language for AWS Lambda](https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/) +. You’re going to create an application that uses an [API Gateway](https://aws.amazon.com/apigateway) event source to +create a simple Hello World RESTful API. ### Build gorush binary @@ -1298,7 +1376,8 @@ we need to build a binary that will run on Linux, and ZIP it up into a deploymen zip deployment.zip release/linux/lambda/gorush ``` -Upload the `deployment.zip` via web UI or you can try the [drone-lambda](https://github.com/appleboy/drone-lambda) as the following command. it will zip your binary file and upload to AWS Lambda automatically. +Upload the `deployment.zip` via web UI or you can try the [drone-lambda](https://github.com/appleboy/drone-lambda) as +the following command. it will zip your binary file and upload to AWS Lambda automatically. ```sh $ AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID \ @@ -1310,7 +1389,9 @@ $ AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID \ ### Without an AWS account -Or you can deploy gorush to alternative solution like [netlify functions](https://docs.netlify.com/functions/overview/). [Netlify](https://www.netlify.com/) lets you deploy serverless Lambda functions without an AWS account, and with function management handled directly within Netlify. Please see the netlify.toml file: +Or you can deploy gorush to alternative solution like [netlify functions](https://docs.netlify.com/functions/overview/) +. [Netlify](https://www.netlify.com/) lets you deploy serverless Lambda functions without an AWS account, and with +function management handled directly within Netlify. Please see the netlify.toml file: ```toml [build] diff --git a/config/config.go b/config/config.go index 6f2270a8a..8e3a97271 100644 --- a/config/config.go +++ b/config/config.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/viper" ) -//nolint +// nolint var defaultConf = []byte(` core: enabled: true # enable httpd server @@ -44,7 +44,6 @@ grpc: port: 9000 api: - push_uri: "/api/push" stat_go_uri: "/api/stat/go" stat_app_uri: "/api/stat/app" config_uri: "/api/config" @@ -52,16 +51,29 @@ api: metric_uri: "/metrics" health_uri: "/healthz" -android: - enabled: true - apikey: "YOUR_API_KEY" - max_retry: 0 # resend fail notification, default value zero is disabled - -huawei: - enabled: false - appsecret: "YOUR_APP_SECRET" - appid: "YOUR_APP_ID" - max_retry: 0 # resend fail notification, default value zero is disabled +tenants: + - tenant_id1: + push_uri: "/push/uri/tenant1" + android: + enabled: true + api_key: "YOUR_API_KEY" + max_retry: 0 # resend fail notification, default value zero is disabled + ios: + enabled: false + key_path: "" + key_base64: "" # load iOS key from base64 input + key_type: "pem" # could be pem, p12 or p8 type + password: "" # certificate password, default as empty string. + production: false + max_concurrent_pushes: 100 # just for push ios notification + max_retry: 0 # resend fail notification, default value zero is disabled + key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) + team_id: "" # TeamID from developer account (View Account -> Membership) + huawei: + enabled: false + api_key: "YOUR_APP_SECRET" + app_id: "YOUR_APP_ID" + max_retry: 0 # resend fail notification, default value zero is disabled queue: engine: "local" # support "local", "nsq", "nats" and "redis" default value is "local" @@ -79,18 +91,6 @@ queue: consumer: gorush stream_name: gorush -ios: - enabled: false - key_path: "" - key_base64: "" # load iOS key from base64 input - key_type: "pem" # could be pem, p12 or p8 type - password: "" # certificate password, default as empty string. - production: false - max_concurrent_pushes: 100 # just for push ios notification - max_retry: 0 # resend fail notification, default value zero is disabled - key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) - team_id: "" # TeamID from developer account (View Account -> Membership) - log: format: "string" # string or json access_log: "stdout" # stdout: output to console, or define log path like "log/access_log" @@ -120,15 +120,13 @@ stat: // ConfYaml is config structure. type ConfYaml struct { - Core SectionCore `yaml:"core"` - API SectionAPI `yaml:"api"` - Android SectionAndroid `yaml:"android"` - Huawei SectionHuawei `yaml:"huawei"` - Ios SectionIos `yaml:"ios"` - Queue SectionQueue `yaml:"queue"` - Log SectionLog `yaml:"log"` - Stat SectionStat `yaml:"stat"` - GRPC SectionGRPC `yaml:"grpc"` + Core SectionCore `yaml:"core"` + API SectionAPI `yaml:"api"` + Tenants map[string]*SectionTenant `mapstructure:"tenants"` + Queue SectionQueue `yaml:"queue"` + Log SectionLog `yaml:"log"` + Stat SectionStat `yaml:"stat"` + GRPC SectionGRPC `yaml:"grpc"` } // SectionCore is sub section of config. @@ -161,9 +159,8 @@ type SectionAutoTLS struct { Host string `yaml:"host"` } -// SectionAPI is sub section of config. +// SectionAPI is subsection of config. type SectionAPI struct { - PushURI string `yaml:"push_uri"` StatGoURI string `yaml:"stat_go_uri"` StatAppURI string `yaml:"stat_app_uri"` ConfigURI string `yaml:"config_uri"` @@ -172,36 +169,44 @@ type SectionAPI struct { HealthURI string `yaml:"health_uri"` } -// SectionAndroid is sub section of config. +// SectionTenant is subsection of config. +type SectionTenant struct { + PushURI string `mapstructure:"push_uri"` + Android SectionAndroid `mapstructure:"android"` + Huawei SectionHuawei `mapstructure:"huawei"` + Ios SectionIos `mapstructure:"ios"` +} + +// SectionAndroid is subsection of tenant. type SectionAndroid struct { - Enabled bool `yaml:"enabled"` - APIKey string `yaml:"apikey"` - MaxRetry int `yaml:"max_retry"` + Enabled bool `mapstructure:"enabled"` + APIKey string `mapstructure:"api_key"` + MaxRetry int `mapstructure:"max_retry"` } -// SectionHuawei is sub section of config. +// SectionHuawei is subsection of tenant. type SectionHuawei struct { - Enabled bool `yaml:"enabled"` - AppSecret string `yaml:"appsecret"` - AppID string `yaml:"appid"` - MaxRetry int `yaml:"max_retry"` + Enabled bool `mapstructure:"enabled"` + APIKey string `mapstructure:"api_key"` + APPId string `mapstructure:"app_id"` + MaxRetry int `mapstructure:"max_retry"` } -// SectionIos is sub section of config. +// SectionIos is subsection of tenant. type SectionIos struct { - Enabled bool `yaml:"enabled"` - KeyPath string `yaml:"key_path"` - KeyBase64 string `yaml:"key_base64"` - KeyType string `yaml:"key_type"` - Password string `yaml:"password"` - Production bool `yaml:"production"` - MaxConcurrentPushes uint `yaml:"max_concurrent_pushes"` - MaxRetry int `yaml:"max_retry"` - KeyID string `yaml:"key_id"` - TeamID string `yaml:"team_id"` + Enabled bool `mapstructure:"enabled"` + KeyPath string `mapstructure:"key_path"` + KeyBase64 string `mapstructure:"key_base64"` + KeyType string `mapstructure:"key_type"` + Password string `mapstructure:"password"` + Production bool `mapstructure:"production"` + MaxConcurrentPushes uint `mapstructure:"max_concurrent_pushes"` + MaxRetry int `mapstructure:"max_retry"` + KeyID string `mapstructure:"key_id"` + TeamID string `mapstructure:"team_id"` } -// SectionLog is sub section of config. +// SectionLog is subsection of config. type SectionLog struct { Format string `yaml:"format"` AccessLog string `yaml:"access_log"` @@ -212,7 +217,7 @@ type SectionLog struct { HideMessages bool `yaml:"hide_messages"` } -// SectionStat is sub section of config. +// SectionStat is subsection of config. type SectionStat struct { Engine string `yaml:"engine"` Redis SectionRedis `yaml:"redis"` @@ -222,7 +227,7 @@ type SectionStat struct { BadgerDB SectionBadgerDB `yaml:"badgerdb"` } -// SectionQueue is sub section of config. +// SectionQueue is subsection of config. type SectionQueue struct { Engine string `yaml:"engine"` NSQ SectionNSQ `yaml:"nsq"` @@ -230,21 +235,21 @@ type SectionQueue struct { Redis SectionRedisQueue `yaml:"redis"` } -// SectionNSQ is sub section of config. +// SectionNSQ is subsection of config. type SectionNSQ struct { Addr string `yaml:"addr"` Topic string `yaml:"topic"` Channel string `yaml:"channel"` } -// SectionNATS is sub section of config. +// SectionNATS is subsection of config. type SectionNATS struct { Addr string `yaml:"addr"` Subj string `yaml:"subj"` Queue string `yaml:"queue"` } -// SectionRedisQueue is sub section of config. +// SectionRedisQueue is subsection of config. type SectionRedisQueue struct { Addr string `yaml:"addr"` StreamName string `yaml:"stream_name"` @@ -252,7 +257,7 @@ type SectionRedisQueue struct { Consumer string `yaml:"consumer"` } -// SectionRedis is sub section of config. +// SectionRedis is subsection of config. type SectionRedis struct { Cluster bool `yaml:"cluster"` Addr string `yaml:"addr"` @@ -260,35 +265,35 @@ type SectionRedis struct { DB int `yaml:"db"` } -// SectionBoltDB is sub section of config. +// SectionBoltDB is subsection of config. type SectionBoltDB struct { Path string `yaml:"path"` Bucket string `yaml:"bucket"` } -// SectionBuntDB is sub section of config. +// SectionBuntDB is subsection of config. type SectionBuntDB struct { Path string `yaml:"path"` } -// SectionLevelDB is sub section of config. +// SectionLevelDB is subsection of config. type SectionLevelDB struct { Path string `yaml:"path"` } -// SectionBadgerDB is sub section of config. +// SectionBadgerDB is subsection of config. type SectionBadgerDB struct { Path string `yaml:"path"` } -// SectionPID is sub section of config. +// SectionPID is subsection of config. type SectionPID struct { Enabled bool `yaml:"enabled"` Path string `yaml:"path"` Override bool `yaml:"override"` } -// SectionGRPC is sub section of config. +// SectionGRPC is subsection of config. type SectionGRPC struct { Enabled bool `yaml:"enabled"` Port string `yaml:"port"` @@ -361,7 +366,6 @@ func LoadConf(confPath ...string) (*ConfYaml, error) { conf.Core.AutoTLS.Host = viper.GetString("core.auto_tls.host") // Api - conf.API.PushURI = viper.GetString("api.push_uri") conf.API.StatGoURI = viper.GetString("api.stat_go_uri") conf.API.StatAppURI = viper.GetString("api.stat_app_uri") conf.API.ConfigURI = viper.GetString("api.config_uri") @@ -369,28 +373,11 @@ func LoadConf(confPath ...string) (*ConfYaml, error) { conf.API.MetricURI = viper.GetString("api.metric_uri") conf.API.HealthURI = viper.GetString("api.health_uri") - // Android - conf.Android.Enabled = viper.GetBool("android.enabled") - conf.Android.APIKey = viper.GetString("android.apikey") - conf.Android.MaxRetry = viper.GetInt("android.max_retry") - - // Huawei - conf.Huawei.Enabled = viper.GetBool("huawei.enabled") - conf.Huawei.AppSecret = viper.GetString("huawei.appsecret") - conf.Huawei.AppID = viper.GetString("huawei.appid") - conf.Huawei.MaxRetry = viper.GetInt("huawei.max_retry") - - // iOS - conf.Ios.Enabled = viper.GetBool("ios.enabled") - conf.Ios.KeyPath = viper.GetString("ios.key_path") - conf.Ios.KeyBase64 = viper.GetString("ios.key_base64") - conf.Ios.KeyType = viper.GetString("ios.key_type") - conf.Ios.Password = viper.GetString("ios.password") - conf.Ios.Production = viper.GetBool("ios.production") - conf.Ios.MaxConcurrentPushes = viper.GetUint("ios.max_concurrent_pushes") - conf.Ios.MaxRetry = viper.GetInt("ios.max_retry") - conf.Ios.KeyID = viper.GetString("ios.key_id") - conf.Ios.TeamID = viper.GetString("ios.team_id") + // Tenants + err := viper.UnmarshalKey("tenants", &conf.Tenants) + if err != nil { + fmt.Print("could not unmarshal tenants") + } // log conf.Log.Format = viper.GetString("log.format") diff --git a/config/config_test.go b/config/config_test.go index 8bf1ff6c0..f13191f95 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -17,15 +17,6 @@ func TestMissingFile(t *testing.T) { assert.NotNil(t, err) } -func TestEmptyConfig(t *testing.T) { - conf, err := LoadConf("testdata/empty.yml") - if err != nil { - panic("failed to load config.yml from file") - } - - assert.Equal(t, uint(100), conf.Ios.MaxConcurrentPushes) -} - type ConfigTestSuite struct { suite.Suite ConfGorushDefault *ConfYaml @@ -46,97 +37,98 @@ func (suite *ConfigTestSuite) SetupTest() { func (suite *ConfigTestSuite) TestValidateConfDefault() { // Core - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.Address) - assert.Equal(suite.T(), "8088", suite.ConfGorushDefault.Core.Port) - assert.Equal(suite.T(), int64(30), suite.ConfGorushDefault.Core.ShutdownTimeout) - assert.Equal(suite.T(), true, suite.ConfGorushDefault.Core.Enabled) - assert.Equal(suite.T(), int64(runtime.NumCPU()), suite.ConfGorushDefault.Core.WorkerNum) - assert.Equal(suite.T(), int64(8192), suite.ConfGorushDefault.Core.QueueNum) - assert.Equal(suite.T(), "release", suite.ConfGorushDefault.Core.Mode) - assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.Sync) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.FeedbackURL) - assert.Equal(suite.T(), int64(10), suite.ConfGorushDefault.Core.FeedbackTimeout) - assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.SSL) - assert.Equal(suite.T(), "cert.pem", suite.ConfGorushDefault.Core.CertPath) - assert.Equal(suite.T(), "key.pem", suite.ConfGorushDefault.Core.KeyPath) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.KeyBase64) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.CertBase64) - assert.Equal(suite.T(), int64(100), suite.ConfGorushDefault.Core.MaxNotification) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.HTTPProxy) + assert.Equal(suite.T(), "", suite.ConfGorush.Core.Address) + assert.Equal(suite.T(), "8088", suite.ConfGorush.Core.Port) + assert.Equal(suite.T(), int64(30), suite.ConfGorush.Core.ShutdownTimeout) + assert.Equal(suite.T(), true, suite.ConfGorush.Core.Enabled) + assert.Equal(suite.T(), int64(runtime.NumCPU()), suite.ConfGorush.Core.WorkerNum) + assert.Equal(suite.T(), int64(8192), suite.ConfGorush.Core.QueueNum) + assert.Equal(suite.T(), "release", suite.ConfGorush.Core.Mode) + assert.Equal(suite.T(), false, suite.ConfGorush.Core.Sync) + assert.Equal(suite.T(), "", suite.ConfGorush.Core.FeedbackURL) + assert.Equal(suite.T(), int64(10), suite.ConfGorush.Core.FeedbackTimeout) + assert.Equal(suite.T(), false, suite.ConfGorush.Core.SSL) + assert.Equal(suite.T(), "cert.pem", suite.ConfGorush.Core.CertPath) + assert.Equal(suite.T(), "key.pem", suite.ConfGorush.Core.KeyPath) + assert.Equal(suite.T(), "", suite.ConfGorush.Core.KeyBase64) + assert.Equal(suite.T(), "", suite.ConfGorush.Core.CertBase64) + assert.Equal(suite.T(), int64(100), suite.ConfGorush.Core.MaxNotification) + assert.Equal(suite.T(), "", suite.ConfGorush.Core.HTTPProxy) // Pid - assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.PID.Enabled) - assert.Equal(suite.T(), "gorush.pid", suite.ConfGorushDefault.Core.PID.Path) - assert.Equal(suite.T(), true, suite.ConfGorushDefault.Core.PID.Override) - assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.AutoTLS.Enabled) - assert.Equal(suite.T(), ".cache", suite.ConfGorushDefault.Core.AutoTLS.Folder) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.AutoTLS.Host) + assert.Equal(suite.T(), false, suite.ConfGorush.Core.PID.Enabled) + assert.Equal(suite.T(), "gorush.pid", suite.ConfGorush.Core.PID.Path) + assert.Equal(suite.T(), true, suite.ConfGorush.Core.PID.Override) + assert.Equal(suite.T(), false, suite.ConfGorush.Core.AutoTLS.Enabled) + assert.Equal(suite.T(), ".cache", suite.ConfGorush.Core.AutoTLS.Folder) + assert.Equal(suite.T(), "", suite.ConfGorush.Core.AutoTLS.Host) // Api - assert.Equal(suite.T(), "/api/push", suite.ConfGorushDefault.API.PushURI) - assert.Equal(suite.T(), "/api/stat/go", suite.ConfGorushDefault.API.StatGoURI) - assert.Equal(suite.T(), "/api/stat/app", suite.ConfGorushDefault.API.StatAppURI) - assert.Equal(suite.T(), "/api/config", suite.ConfGorushDefault.API.ConfigURI) - assert.Equal(suite.T(), "/sys/stats", suite.ConfGorushDefault.API.SysStatURI) - assert.Equal(suite.T(), "/metrics", suite.ConfGorushDefault.API.MetricURI) - assert.Equal(suite.T(), "/healthz", suite.ConfGorushDefault.API.HealthURI) + assert.Equal(suite.T(), "/api/stat/go", suite.ConfGorush.API.StatGoURI) + assert.Equal(suite.T(), "/api/stat/app", suite.ConfGorush.API.StatAppURI) + assert.Equal(suite.T(), "/api/config", suite.ConfGorush.API.ConfigURI) + assert.Equal(suite.T(), "/sys/stats", suite.ConfGorush.API.SysStatURI) + assert.Equal(suite.T(), "/metrics", suite.ConfGorush.API.MetricURI) + assert.Equal(suite.T(), "/healthz", suite.ConfGorush.API.HealthURI) + tenant := suite.ConfGorush.Tenants["tenant_id1"] + assert.Equal(suite.T(), "/api/push/tenant1", tenant.PushURI) // Android - assert.Equal(suite.T(), true, suite.ConfGorushDefault.Android.Enabled) - assert.Equal(suite.T(), "YOUR_API_KEY", suite.ConfGorushDefault.Android.APIKey) - assert.Equal(suite.T(), 0, suite.ConfGorushDefault.Android.MaxRetry) + assert.Equal(suite.T(), true, tenant.Android.Enabled) + assert.Equal(suite.T(), "YOUR_API_KEY", tenant.Android.APIKey) + assert.Equal(suite.T(), 0, tenant.Android.MaxRetry) // iOS - assert.Equal(suite.T(), false, suite.ConfGorushDefault.Ios.Enabled) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.KeyPath) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.KeyBase64) - assert.Equal(suite.T(), "pem", suite.ConfGorushDefault.Ios.KeyType) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.Password) - assert.Equal(suite.T(), false, suite.ConfGorushDefault.Ios.Production) - assert.Equal(suite.T(), uint(100), suite.ConfGorushDefault.Ios.MaxConcurrentPushes) - assert.Equal(suite.T(), 0, suite.ConfGorushDefault.Ios.MaxRetry) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.KeyID) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.TeamID) + assert.Equal(suite.T(), false, tenant.Ios.Enabled) + assert.Equal(suite.T(), "key.pem", tenant.Ios.KeyPath) + assert.Equal(suite.T(), "", tenant.Ios.KeyBase64) + assert.Equal(suite.T(), "pem", tenant.Ios.KeyType) + assert.Equal(suite.T(), "", tenant.Ios.Password) + assert.Equal(suite.T(), false, tenant.Ios.Production) + assert.Equal(suite.T(), uint(100), tenant.Ios.MaxConcurrentPushes) + assert.Equal(suite.T(), 0, tenant.Ios.MaxRetry) + assert.Equal(suite.T(), "", tenant.Ios.KeyID) + assert.Equal(suite.T(), "", tenant.Ios.TeamID) // queue - assert.Equal(suite.T(), "local", suite.ConfGorushDefault.Queue.Engine) - assert.Equal(suite.T(), "127.0.0.1:4150", suite.ConfGorushDefault.Queue.NSQ.Addr) - assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.NSQ.Topic) - assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.NSQ.Channel) + assert.Equal(suite.T(), "local", suite.ConfGorush.Queue.Engine) + assert.Equal(suite.T(), "127.0.0.1:4150", suite.ConfGorush.Queue.NSQ.Addr) + assert.Equal(suite.T(), "gorush", suite.ConfGorush.Queue.NSQ.Topic) + assert.Equal(suite.T(), "gorush", suite.ConfGorush.Queue.NSQ.Channel) - assert.Equal(suite.T(), "127.0.0.1:4222", suite.ConfGorushDefault.Queue.NATS.Addr) - assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.NATS.Subj) - assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.NATS.Queue) + assert.Equal(suite.T(), "127.0.0.1:4222", suite.ConfGorush.Queue.NATS.Addr) + assert.Equal(suite.T(), "gorush", suite.ConfGorush.Queue.NATS.Subj) + assert.Equal(suite.T(), "gorush", suite.ConfGorush.Queue.NATS.Queue) - assert.Equal(suite.T(), "127.0.0.1:6379", suite.ConfGorushDefault.Queue.Redis.Addr) - assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.Redis.StreamName) - assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.Redis.Group) - assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Queue.Redis.Consumer) + assert.Equal(suite.T(), "127.0.0.1:6379", suite.ConfGorush.Queue.Redis.Addr) + assert.Equal(suite.T(), "gorush", suite.ConfGorush.Queue.Redis.StreamName) + assert.Equal(suite.T(), "gorush", suite.ConfGorush.Queue.Redis.Group) + assert.Equal(suite.T(), "gorush", suite.ConfGorush.Queue.Redis.Consumer) // log - assert.Equal(suite.T(), "string", suite.ConfGorushDefault.Log.Format) - assert.Equal(suite.T(), "stdout", suite.ConfGorushDefault.Log.AccessLog) - assert.Equal(suite.T(), "debug", suite.ConfGorushDefault.Log.AccessLevel) - assert.Equal(suite.T(), "stderr", suite.ConfGorushDefault.Log.ErrorLog) - assert.Equal(suite.T(), "error", suite.ConfGorushDefault.Log.ErrorLevel) - assert.Equal(suite.T(), true, suite.ConfGorushDefault.Log.HideToken) - assert.Equal(suite.T(), false, suite.ConfGorushDefault.Log.HideMessages) - - assert.Equal(suite.T(), "memory", suite.ConfGorushDefault.Stat.Engine) - assert.Equal(suite.T(), false, suite.ConfGorushDefault.Stat.Redis.Cluster) - assert.Equal(suite.T(), "localhost:6379", suite.ConfGorushDefault.Stat.Redis.Addr) - assert.Equal(suite.T(), "", suite.ConfGorushDefault.Stat.Redis.Password) - assert.Equal(suite.T(), 0, suite.ConfGorushDefault.Stat.Redis.DB) - - assert.Equal(suite.T(), "bolt.db", suite.ConfGorushDefault.Stat.BoltDB.Path) - assert.Equal(suite.T(), "gorush", suite.ConfGorushDefault.Stat.BoltDB.Bucket) - - assert.Equal(suite.T(), "bunt.db", suite.ConfGorushDefault.Stat.BuntDB.Path) - assert.Equal(suite.T(), "level.db", suite.ConfGorushDefault.Stat.LevelDB.Path) - assert.Equal(suite.T(), "badger.db", suite.ConfGorushDefault.Stat.BadgerDB.Path) + assert.Equal(suite.T(), "string", suite.ConfGorush.Log.Format) + assert.Equal(suite.T(), "stdout", suite.ConfGorush.Log.AccessLog) + assert.Equal(suite.T(), "debug", suite.ConfGorush.Log.AccessLevel) + assert.Equal(suite.T(), "stderr", suite.ConfGorush.Log.ErrorLog) + assert.Equal(suite.T(), "error", suite.ConfGorush.Log.ErrorLevel) + assert.Equal(suite.T(), true, suite.ConfGorush.Log.HideToken) + assert.Equal(suite.T(), false, suite.ConfGorush.Log.HideMessages) + + assert.Equal(suite.T(), "memory", suite.ConfGorush.Stat.Engine) + assert.Equal(suite.T(), false, suite.ConfGorush.Stat.Redis.Cluster) + assert.Equal(suite.T(), "localhost:6379", suite.ConfGorush.Stat.Redis.Addr) + assert.Equal(suite.T(), "", suite.ConfGorush.Stat.Redis.Password) + assert.Equal(suite.T(), 0, suite.ConfGorush.Stat.Redis.DB) + + assert.Equal(suite.T(), "bolt.db", suite.ConfGorush.Stat.BoltDB.Path) + assert.Equal(suite.T(), "gorush", suite.ConfGorush.Stat.BoltDB.Bucket) + + assert.Equal(suite.T(), "bunt.db", suite.ConfGorush.Stat.BuntDB.Path) + assert.Equal(suite.T(), "level.db", suite.ConfGorush.Stat.LevelDB.Path) + assert.Equal(suite.T(), "badger.db", suite.ConfGorush.Stat.BadgerDB.Path) // gRPC - assert.Equal(suite.T(), false, suite.ConfGorushDefault.GRPC.Enabled) - assert.Equal(suite.T(), "9000", suite.ConfGorushDefault.GRPC.Port) + assert.Equal(suite.T(), false, suite.ConfGorush.GRPC.Enabled) + assert.Equal(suite.T(), "9000", suite.ConfGorush.GRPC.Port) } func (suite *ConfigTestSuite) TestValidateConf() { @@ -166,7 +158,6 @@ func (suite *ConfigTestSuite) TestValidateConf() { assert.Equal(suite.T(), "", suite.ConfGorush.Core.AutoTLS.Host) // Api - assert.Equal(suite.T(), "/api/push", suite.ConfGorush.API.PushURI) assert.Equal(suite.T(), "/api/stat/go", suite.ConfGorush.API.StatGoURI) assert.Equal(suite.T(), "/api/stat/app", suite.ConfGorush.API.StatAppURI) assert.Equal(suite.T(), "/api/config", suite.ConfGorush.API.ConfigURI) @@ -175,21 +166,21 @@ func (suite *ConfigTestSuite) TestValidateConf() { assert.Equal(suite.T(), "/healthz", suite.ConfGorush.API.HealthURI) // Android - assert.Equal(suite.T(), true, suite.ConfGorush.Android.Enabled) - assert.Equal(suite.T(), "YOUR_API_KEY", suite.ConfGorush.Android.APIKey) - assert.Equal(suite.T(), 0, suite.ConfGorush.Android.MaxRetry) + assert.Equal(suite.T(), true, suite.ConfGorush.Tenants["tenant_id1"].Android.Enabled) + assert.Equal(suite.T(), "YOUR_API_KEY", suite.ConfGorush.Tenants["tenant_id1"].Android.APIKey) + assert.Equal(suite.T(), 0, suite.ConfGorush.Tenants["tenant_id1"].Android.MaxRetry) // iOS - assert.Equal(suite.T(), false, suite.ConfGorush.Ios.Enabled) - assert.Equal(suite.T(), "key.pem", suite.ConfGorush.Ios.KeyPath) - assert.Equal(suite.T(), "", suite.ConfGorush.Ios.KeyBase64) - assert.Equal(suite.T(), "pem", suite.ConfGorush.Ios.KeyType) - assert.Equal(suite.T(), "", suite.ConfGorush.Ios.Password) - assert.Equal(suite.T(), false, suite.ConfGorush.Ios.Production) - assert.Equal(suite.T(), uint(100), suite.ConfGorush.Ios.MaxConcurrentPushes) - assert.Equal(suite.T(), 0, suite.ConfGorush.Ios.MaxRetry) - assert.Equal(suite.T(), "", suite.ConfGorush.Ios.KeyID) - assert.Equal(suite.T(), "", suite.ConfGorush.Ios.TeamID) + assert.Equal(suite.T(), false, suite.ConfGorush.Tenants["tenant_id1"].Ios.Enabled) + assert.Equal(suite.T(), "key.pem", suite.ConfGorush.Tenants["tenant_id1"].Ios.KeyPath) + assert.Equal(suite.T(), "", suite.ConfGorush.Tenants["tenant_id1"].Ios.KeyBase64) + assert.Equal(suite.T(), "pem", suite.ConfGorush.Tenants["tenant_id1"].Ios.KeyType) + assert.Equal(suite.T(), "", suite.ConfGorush.Tenants["tenant_id1"].Ios.Password) + assert.Equal(suite.T(), false, suite.ConfGorush.Tenants["tenant_id1"].Ios.Production) + assert.Equal(suite.T(), uint(100), suite.ConfGorush.Tenants["tenant_id1"].Ios.MaxConcurrentPushes) + assert.Equal(suite.T(), 0, suite.ConfGorush.Tenants["tenant_id1"].Ios.MaxRetry) + assert.Equal(suite.T(), "", suite.ConfGorush.Tenants["tenant_id1"].Ios.KeyID) + assert.Equal(suite.T(), "", suite.ConfGorush.Tenants["tenant_id1"].Ios.TeamID) // log assert.Equal(suite.T(), "string", suite.ConfGorush.Log.Format) @@ -225,8 +216,6 @@ func TestLoadConfigFromEnv(t *testing.T) { os.Setenv("GORUSH_CORE_PORT", "9001") os.Setenv("GORUSH_GRPC_ENABLED", "true") os.Setenv("GORUSH_CORE_MAX_NOTIFICATION", "200") - os.Setenv("GORUSH_IOS_KEY_ID", "ABC123DEFG") - os.Setenv("GORUSH_IOS_TEAM_ID", "DEF123GHIJ") os.Setenv("GORUSH_API_HEALTH_URI", "/healthz") ConfGorush, err := LoadConf("testdata/config.yml") if err != nil { @@ -235,13 +224,5 @@ func TestLoadConfigFromEnv(t *testing.T) { assert.Equal(t, "9001", ConfGorush.Core.Port) assert.Equal(t, int64(200), ConfGorush.Core.MaxNotification) assert.True(t, ConfGorush.GRPC.Enabled) - assert.Equal(t, "ABC123DEFG", ConfGorush.Ios.KeyID) - assert.Equal(t, "DEF123GHIJ", ConfGorush.Ios.TeamID) assert.Equal(t, "/healthz", ConfGorush.API.HealthURI) } - -func TestLoadWrongDefaultYAMLConfig(t *testing.T) { - defaultConf = []byte(`a`) - _, err := LoadConf() - assert.Error(t, err) -} diff --git a/config/testdata/config.yml b/config/testdata/config.yml index 55a95802f..c6fb065d9 100644 --- a/config/testdata/config.yml +++ b/config/testdata/config.yml @@ -30,7 +30,6 @@ grpc: port: 9000 api: - push_uri: "/api/push" stat_go_uri: "/api/stat/go" stat_app_uri: "/api/stat/app" config_uri: "/api/config" @@ -38,16 +37,29 @@ api: metric_uri: "/metrics" health_uri: "/healthz" -android: - enabled: true - apikey: "YOUR_API_KEY" - max_retry: 0 # resend fail notification, default value zero is disabled - -huawei: - enabled: false - appsecret: "YOUR_APP_SECRET" - appid: "YOUR_APP_ID" - max_retry: 0 # resend fail notification, default value zero is disabled +tenants: + - tenant_id1: + push_uri: "/api/push/tenant1" # must be in the form of /api/push/:tenant_id + android: + enabled: true + api_key: "YOUR_API_KEY" + max_retry: 0 # resend fail notification, default value zero is disabled + ios: + enabled: false + key_path: "key.pem" + key_base64: "" # load iOS key from base64 input + key_type: "pem" # could be pem, p12 or p8 type + password: "" # certificate password, default as empty string. + production: false + max_concurrent_pushes: 100 # just for push ios notification + max_retry: 0 # resend fail notification, default value zero is disabled + key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) + team_id: "" # TeamID from developer account (View Account -> Membership) + huawei: + enabled: true + api_key: "YOUR_API_KEY" + app_id: "YOUR_APP_ID" + max_retry: 0 # resend fail notification, default value zero is disabled queue: engine: "local" # support "local", "nsq", "nats" and "redis" default value is "local" @@ -65,18 +77,6 @@ queue: consumer: gorush stream_name: gorush -ios: - enabled: false - key_path: "key.pem" - key_base64: "" # load iOS key from base64 input - key_type: "pem" # could be pem, p12 or p8 type - password: "" # certificate password, default as empty string. - production: false - max_concurrent_pushes: 100 # just for push ios notification - max_retry: 0 # resend fail notification, default value zero is disabled - key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) - team_id: "" # TeamID from developer account (View Account -> Membership) - log: format: "string" # string or json access_log: "stdout" # stdout: output to console, or define log path like "log/access_log" diff --git a/config/testdata/empty.yml b/config/testdata/empty.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/gorush/const.go b/gorush/const.go new file mode 100644 index 000000000..af1d481e4 --- /dev/null +++ b/gorush/const.go @@ -0,0 +1,17 @@ +package gorush + +const ( + // PlatformIos constant is 1 for iOS + PlatformIos = iota + 1 + // PlatformAndroid constant is 2 for Android + PlatformAndroid + // PlatformHuawei constant is 3 for Huawei + PlatformHuawei +) + +const ( + // SucceededPush is log block + SucceededPush = "succeeded-push" + // FailedPush is log block + FailedPush = "failed-push" +) diff --git a/logx/log.go b/logx/log.go index 922ea70c3..04455a24e 100644 --- a/logx/log.go +++ b/logx/log.go @@ -23,6 +23,7 @@ var ( // LogPushEntry is push response log type LogPushEntry struct { + TenantId string `json:"tenant_id"` ID string `json:"notif_id,omitempty"` Type string `json:"type"` Platform string `json:"platform"` @@ -33,7 +34,7 @@ type LogPushEntry struct { var isTerm bool -//nolint +// nolint func init() { isTerm = isatty.IsTerminal(os.Stdout.Fd()) } @@ -181,6 +182,7 @@ func GetLogPushEntry(input *InputLog) LogPushEntry { } return LogPushEntry{ + TenantId: input.TenantId, ID: input.ID, Type: input.Status, Platform: plat, @@ -193,6 +195,7 @@ func GetLogPushEntry(input *InputLog) LogPushEntry { // InputLog log request type InputLog struct { ID string + TenantId string Status string Token string Message string diff --git a/main.go b/main.go index 8d25109aa..684f86f0f 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,9 @@ import ( "context" "flag" "fmt" + "github.com/appleboy/gorush/gorush" + core2 "github.com/msalihkarakasli/go-hms-push/push/core" + "github.com/sideshow/apns2" "log" "net" "net/http" @@ -12,6 +15,7 @@ import ( "strconv" "time" + "github.com/appleboy/go-fcm" "github.com/appleboy/gorush/config" "github.com/appleboy/gorush/core" "github.com/appleboy/gorush/logx" @@ -40,25 +44,20 @@ func main() { message string token string title string + tenantId string ) + defaultTenant := &config.SectionTenant{ + PushURI: "", + Android: config.SectionAndroid{}, + Huawei: config.SectionHuawei{}, + Ios: config.SectionIos{}, + } flag.BoolVar(&showVersion, "version", false, "Print version information.") flag.BoolVar(&showVersion, "V", false, "Print version information.") flag.StringVar(&configFile, "c", "", "Configuration file path.") flag.StringVar(&configFile, "config", "", "Configuration file path.") flag.StringVar(&opts.Core.PID.Path, "pid", "", "PID file path.") - flag.StringVar(&opts.Ios.KeyPath, "i", "", "iOS certificate key file path") - flag.StringVar(&opts.Ios.KeyPath, "key", "", "iOS certificate key file path") - flag.StringVar(&opts.Ios.KeyID, "key-id", "", "iOS Key ID for P8 token") - flag.StringVar(&opts.Ios.TeamID, "team-id", "", "iOS Team ID for P8 token") - flag.StringVar(&opts.Ios.Password, "P", "", "iOS certificate password for gorush") - flag.StringVar(&opts.Ios.Password, "password", "", "iOS certificate password for gorush") - flag.StringVar(&opts.Android.APIKey, "k", "", "Android api key configuration for gorush") - flag.StringVar(&opts.Android.APIKey, "apikey", "", "Android api key configuration for gorush") - flag.StringVar(&opts.Huawei.AppSecret, "hk", "", "Huawei api key configuration for gorush") - flag.StringVar(&opts.Huawei.AppSecret, "hmskey", "", "Huawei api key configuration for gorush") - flag.StringVar(&opts.Huawei.AppID, "hid", "", "HMS app id configuration for gorush") - flag.StringVar(&opts.Huawei.AppID, "hmsid", "", "HMS app id configuration for gorush") flag.StringVar(&opts.Core.Address, "A", "", "address to bind") flag.StringVar(&opts.Core.Address, "address", "", "address to bind") flag.StringVar(&opts.Core.Port, "p", "", "port number for gorush") @@ -68,16 +67,37 @@ func main() { flag.StringVar(&opts.Stat.Engine, "e", "", "store engine") flag.StringVar(&opts.Stat.Engine, "engine", "", "store engine") flag.StringVar(&opts.Stat.Redis.Addr, "redis-addr", "", "redis addr") + flag.StringVar(&opts.Core.HTTPProxy, "proxy", "", "http proxy url") + flag.BoolVar(&ping, "ping", false, "ping server") + flag.StringVar(&message, "m", "", "notification message") flag.StringVar(&message, "message", "", "notification message") flag.StringVar(&title, "title", "", "notification title") - flag.BoolVar(&opts.Android.Enabled, "android", false, "send android notification") - flag.BoolVar(&opts.Huawei.Enabled, "huawei", false, "send huawei notification") - flag.BoolVar(&opts.Ios.Enabled, "ios", false, "send ios notification") - flag.BoolVar(&opts.Ios.Production, "production", false, "production mode in iOS") flag.StringVar(&topic, "topic", "", "apns topic in iOS") - flag.StringVar(&opts.Core.HTTPProxy, "proxy", "", "http proxy url") - flag.BoolVar(&ping, "ping", false, "ping server") + flag.StringVar(&tenantId, "tenantid", "", "tenant id used for notifications") + flag.StringVar(&tenantId, "tid", "", "tenant id used for notifications") + + if tenantId != "" { + opts.Tenants = make(map[string]*config.SectionTenant) + opts.Tenants[tenantId] = defaultTenant + + flag.StringVar(&defaultTenant.Ios.KeyPath, "i", "", "iOS certificate key file path") + flag.StringVar(&defaultTenant.Ios.KeyPath, "key", "", "iOS certificate key file path") + flag.StringVar(&defaultTenant.Ios.KeyID, "key-id", "", "iOS Key ID for P8 token") + flag.StringVar(&defaultTenant.Ios.TeamID, "team-id", "", "iOS Team ID for P8 token") + flag.StringVar(&defaultTenant.Ios.Password, "P", "", "iOS certificate password for gorush") + flag.StringVar(&defaultTenant.Ios.Password, "password", "", "iOS certificate password for gorush") + flag.StringVar(&defaultTenant.Android.APIKey, "k", "", "Android api key configuration for gorush") + flag.StringVar(&defaultTenant.Android.APIKey, "apikey", "", "Android api key configuration for gorush") + flag.StringVar(&defaultTenant.Huawei.APIKey, "hk", "", "Huawei api key configuration for gorush") + flag.StringVar(&defaultTenant.Huawei.APIKey, "hmskey", "", "Huawei api key configuration for gorush") + flag.StringVar(&defaultTenant.Huawei.APPId, "hid", "", "HMS app id configuration for gorush") + flag.StringVar(&defaultTenant.Huawei.APPId, "hmsid", "", "HMS app id configuration for gorush") + flag.BoolVar(&defaultTenant.Android.Enabled, "android", false, "send android notification") + flag.BoolVar(&defaultTenant.Huawei.Enabled, "huawei", false, "send huawei notification") + flag.BoolVar(&defaultTenant.Ios.Enabled, "ios", false, "send ios notification") + flag.BoolVar(&defaultTenant.Ios.Production, "production", false, "production mode in iOS") + } flag.Usage = usage flag.Parse() @@ -99,36 +119,12 @@ func main() { return } - // Initialize push slots for concurrent iOS pushes - notify.MaxConcurrentIOSPushes = make(chan struct{}, cfg.Ios.MaxConcurrentPushes) - - if opts.Ios.KeyPath != "" { - cfg.Ios.KeyPath = opts.Ios.KeyPath - } - - if opts.Ios.KeyID != "" { - cfg.Ios.KeyID = opts.Ios.KeyID - } - - if opts.Ios.TeamID != "" { - cfg.Ios.TeamID = opts.Ios.TeamID + if tenantId != "" && cfg.Tenants[tenantId] == nil { + cfg.Tenants[tenantId] = defaultTenant } - if opts.Ios.Password != "" { - cfg.Ios.Password = opts.Ios.Password - } - - if opts.Android.APIKey != "" { - cfg.Android.APIKey = opts.Android.APIKey - } - - if opts.Huawei.AppSecret != "" { - cfg.Huawei.AppSecret = opts.Huawei.AppSecret - } - - if opts.Huawei.AppID != "" { - cfg.Huawei.AppID = opts.Huawei.AppID - } + // Initialize push slots for concurrent iOS pushes + notify.MaxConcurrentIOSPushes = make(map[string]chan struct{}) if opts.Stat.Engine != "" { cfg.Stat.Engine = opts.Stat.Engine @@ -175,10 +171,10 @@ func main() { } // send android notification - if opts.Android.Enabled { - cfg.Android.Enabled = opts.Android.Enabled + if defaultTenant.Android.Enabled { req := ¬ify.PushNotification{ - Platform: core.PlatFormAndroid, + TenantId: tenantId, + Platform: gorush.PlatformAndroid, Message: message, Title: title, } @@ -210,10 +206,10 @@ func main() { } // send huawei notification - if opts.Huawei.Enabled { - cfg.Huawei.Enabled = opts.Huawei.Enabled + if defaultTenant.Huawei.Enabled { req := ¬ify.PushNotification{ - Platform: core.PlatFormHuawei, + TenantId: tenantId, + Platform: gorush.PlatformHuawei, Message: message, Title: title, } @@ -245,14 +241,10 @@ func main() { } // send ios notification - if opts.Ios.Enabled { - if opts.Ios.Production { - cfg.Ios.Production = opts.Ios.Production - } - - cfg.Ios.Enabled = opts.Ios.Enabled + if defaultTenant.Ios.Enabled { req := ¬ify.PushNotification{ - Platform: core.PlatFormIos, + TenantId: tenantId, + Platform: gorush.PlatformIos, Message: message, Title: title, } @@ -276,7 +268,7 @@ func main() { return } - if err := notify.InitAPNSClient(cfg); err != nil { + if err := notify.InitAPNSClient(tenantId, *defaultTenant); err != nil { return } @@ -365,20 +357,22 @@ func main() { return nil }) - if cfg.Ios.Enabled { - if err = notify.InitAPNSClient(cfg); err != nil { + notify.ApnsClients = make(map[string]*apns2.Client) + notify.FCMClients = make(map[string]*fcm.Client) + notify.HMSClients = make(map[string]*core2.HMSClient) + + for tenantId, tenant := range cfg.Tenants { + if err = notify.InitAPNSClient(tenantId, *tenant); err != nil { logx.LogError.Fatal(err) } - } + // Initialize push slots for concurrent iOS pushes + notify.MaxConcurrentIOSPushes[tenantId] = make(chan struct{}, cfg.Tenants[tenantId].Ios.MaxConcurrentPushes) - if cfg.Android.Enabled { - if _, err = notify.InitFCMClient(cfg, cfg.Android.APIKey); err != nil { + if _, err = notify.InitFCMClient(tenantId, tenant.Android.APIKey); err != nil { logx.LogError.Fatal(err) } - } - if cfg.Huawei.Enabled { - if _, err = notify.InitHMSClient(cfg, cfg.Huawei.AppSecret, cfg.Huawei.AppID); err != nil { + if _, err = notify.InitHMSClient(cfg, tenantId, tenant.Huawei.APIKey, tenant.Huawei.APPId); err != nil { logx.LogError.Fatal(err) } } @@ -438,6 +432,8 @@ Common Options: --topic iOS, Android or Huawei topic message -h, --help Show this message -V, --version Show version + -v, --version Show version + -tid, --tenantid Tenant id to be used ` // usage will print out the flag options for the server. diff --git a/notify/feedback_test.go b/notify/feedback_test.go index ec20b502e..9d36d6145 100644 --- a/notify/feedback_test.go +++ b/notify/feedback_test.go @@ -15,6 +15,7 @@ import ( func TestEmptyFeedbackURL(t *testing.T) { cfg, _ := config.LoadConf() logEntry := logx.LogPushEntry{ + TenantId: "", ID: "", Type: "", Platform: "", @@ -31,6 +32,7 @@ func TestHTTPErrorInFeedbackCall(t *testing.T) { cfg, _ := config.LoadConf() cfg.Core.FeedbackURL = "http://test.example.com/api/" logEntry := logx.LogPushEntry{ + TenantId: "", ID: "", Type: "", Platform: "", @@ -62,6 +64,7 @@ func TestSuccessfulFeedbackCall(t *testing.T) { cfg, _ := config.LoadConf() cfg.Core.FeedbackURL = httpMock.URL logEntry := logx.LogPushEntry{ + TenantId: "", ID: "", Type: "", Platform: "", diff --git a/notify/global.go b/notify/global.go index 19a15a1c2..d11cbaaa9 100644 --- a/notify/global.go +++ b/notify/global.go @@ -7,14 +7,14 @@ import ( ) var ( - // ApnsClient is apns client - ApnsClient *apns2.Client - // FCMClient is apns client - FCMClient *fcm.Client - // HMSClient is Huawei push client - HMSClient *core.HMSClient + // ApnsClients is apns client + ApnsClients map[string]*apns2.Client + // FCMClients is apns client + FCMClients map[string]*fcm.Client + // HMSClients is Huawei push client + HMSClients map[string]*core.HMSClient // MaxConcurrentIOSPushes pool to limit the number of concurrent iOS pushes - MaxConcurrentIOSPushes chan struct{} + MaxConcurrentIOSPushes map[string]chan struct{} ) const ( diff --git a/notify/notification.go b/notify/notification.go index ad40a9894..1dd6feb2c 100644 --- a/notify/notification.go +++ b/notify/notification.go @@ -65,6 +65,8 @@ type ResponsePush struct { // PushNotification is single notification request type PushNotification struct { + TenantId string + // Common ID string `json:"notif_id,omitempty"` Tokens []string `json:"tokens" binding:"required"` @@ -148,7 +150,13 @@ func (p *PushNotification) IsTopic() bool { func CheckMessage(req *PushNotification) error { var msg string - // ignore send topic mesaage from FCM + if req.TenantId == "" { + msg = "the message must specify a tenant ID" + logx.LogAccess.Debug(msg) + return errors.New(msg) + } + + // ignore send topic message from FCM if !req.IsTopic() && len(req.Tokens) == 0 && req.To == "" { msg = "the message must specify at least one registration ID" logx.LogAccess.Debug(msg) @@ -199,36 +207,38 @@ func SetProxy(proxy string) error { // CheckPushConf provide check your yml config. func CheckPushConf(cfg *config.ConfYaml) error { - if !cfg.Ios.Enabled && !cfg.Android.Enabled && !cfg.Huawei.Enabled { - return errors.New("please enable iOS, Android or Huawei config in yml config") - } - - if cfg.Ios.Enabled { - if cfg.Ios.KeyPath == "" && cfg.Ios.KeyBase64 == "" { - return errors.New("missing iOS certificate key") + for tenantId, tenant := range cfg.Tenants { + if !tenant.Ios.Enabled && !tenant.Android.Enabled && !tenant.Huawei.Enabled { + return errors.New("please enable iOS, Android or Huawei config in yml config for tenant " + tenantId) } - // check certificate file exist - if cfg.Ios.KeyPath != "" { - if _, err := os.Stat(cfg.Ios.KeyPath); os.IsNotExist(err) { - return errors.New("certificate file does not exist") + if tenant.Ios.Enabled { + if tenant.Ios.KeyPath == "" && tenant.Ios.KeyBase64 == "" { + return errors.New("missing iOS certificate key for tenant " + tenantId) } - } - } - if cfg.Android.Enabled { - if cfg.Android.APIKey == "" { - return errors.New("missing android api key") + // check certificate file exist + if tenant.Ios.KeyPath != "" { + if _, err := os.Stat(tenant.Ios.KeyPath); os.IsNotExist(err) { + return errors.New("certificate file does not exist for tenant " + tenantId) + } + } } - } - if cfg.Huawei.Enabled { - if cfg.Huawei.AppSecret == "" { - return errors.New("missing huawei app secret") + if tenant.Android.Enabled { + if tenant.Android.APIKey == "" { + return errors.New("missing Android API Key for tenant " + tenantId) + } } - if cfg.Huawei.AppID == "" { - return errors.New("missing huawei app id") + if tenant.Huawei.Enabled { + if tenant.Huawei.APIKey == "" { + return errors.New("missing Huawei API Key for tenant " + tenantId) + } + + if tenant.Huawei.APPId == "" { + return errors.New("missing Huawei APP Id for tenant " + tenantId) + } } } diff --git a/notify/notification_apns.go b/notify/notification_apns.go index 6f053e8b3..8d6ad89b1 100644 --- a/notify/notification_apns.go +++ b/notify/notification_apns.go @@ -58,23 +58,23 @@ type Sound struct { } // InitAPNSClient use for initialize APNs Client. -func InitAPNSClient(cfg *config.ConfYaml) error { - if cfg.Ios.Enabled { +func InitAPNSClient(tenantId string, tenant config.SectionTenant) error { + if tenant.Ios.Enabled { var err error var authKey *ecdsa.PrivateKey var certificateKey tls.Certificate var ext string - if cfg.Ios.KeyPath != "" { - ext = filepath.Ext(cfg.Ios.KeyPath) + if tenant.Ios.KeyPath != "" { + ext = filepath.Ext(tenant.Ios.KeyPath) switch ext { case dotP12: - certificateKey, err = certificate.FromP12File(cfg.Ios.KeyPath, cfg.Ios.Password) + certificateKey, err = certificate.FromP12File(tenant.Ios.KeyPath, tenant.Ios.Password) case dotPEM: - certificateKey, err = certificate.FromPemFile(cfg.Ios.KeyPath, cfg.Ios.Password) + certificateKey, err = certificate.FromPemFile(tenant.Ios.KeyPath, tenant.Ios.Password) case dotP8: - authKey, err = token.AuthKeyFromFile(cfg.Ios.KeyPath) + authKey, err = token.AuthKeyFromFile(tenant.Ios.KeyPath) default: err = errors.New("wrong certificate key extension") } @@ -84,9 +84,9 @@ func InitAPNSClient(cfg *config.ConfYaml) error { return err } - } else if cfg.Ios.KeyBase64 != "" { - ext = "." + cfg.Ios.KeyType - key, err := base64.StdEncoding.DecodeString(cfg.Ios.KeyBase64) + } else if tenant.Ios.KeyBase64 != "" { + ext = "." + tenant.Ios.KeyType + key, err := base64.StdEncoding.DecodeString(tenant.Ios.KeyBase64) if err != nil { logx.LogError.Error("base64 decode error:", err.Error()) @@ -94,9 +94,9 @@ func InitAPNSClient(cfg *config.ConfYaml) error { } switch ext { case dotP12: - certificateKey, err = certificate.FromP12Bytes(key, cfg.Ios.Password) + certificateKey, err = certificate.FromP12Bytes(key, tenant.Ios.Password) case dotPEM: - certificateKey, err = certificate.FromPemBytes(key, cfg.Ios.Password) + certificateKey, err = certificate.FromPemBytes(key, tenant.Ios.Password) case dotP8: authKey, err = token.AuthKeyFromBytes(key) default: @@ -111,25 +111,25 @@ func InitAPNSClient(cfg *config.ConfYaml) error { } if ext == dotP8 { - if cfg.Ios.KeyID == "" || cfg.Ios.TeamID == "" { + if tenant.Ios.KeyID == "" || tenant.Ios.TeamID == "" { msg := "you should provide ios.KeyID and ios.TeamID for p8 token" logx.LogError.Error(msg) return errors.New(msg) } - token := &token.Token{ + jwt := &token.Token{ AuthKey: authKey, // KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) - KeyID: cfg.Ios.KeyID, + KeyID: tenant.Ios.KeyID, // TeamID from developer account (View Account -> Membership) - TeamID: cfg.Ios.TeamID, + TeamID: tenant.Ios.TeamID, } - ApnsClient, err = newApnsTokenClient(cfg, token) + ApnsClients[tenantId], err = newApnsTokenClient(jwt, tenant.Ios.Production) } else { - ApnsClient, err = newApnsClient(cfg, certificateKey) + ApnsClients[tenantId], err = newApnsClient(certificateKey, tenant.Ios.Production) } - if h2Transport, ok := ApnsClient.HTTPClient.Transport.(*http2.Transport); ok { + if h2Transport, ok := ApnsClients[tenantId].HTTPClient.Transport.(*http2.Transport); ok { configureHTTP2ConnHealthCheck(h2Transport) } @@ -140,26 +140,22 @@ func InitAPNSClient(cfg *config.ConfYaml) error { } doOnce.Do(func() { - MaxConcurrentIOSPushes = make(chan struct{}, cfg.Ios.MaxConcurrentPushes) + MaxConcurrentIOSPushes[tenantId] = make(chan struct{}, tenant.Ios.MaxConcurrentPushes) }) } return nil } -func newApnsClient(cfg *config.ConfYaml, certificate tls.Certificate) (*apns2.Client, error) { +func newApnsClient(certificate tls.Certificate, isProduction bool) (*apns2.Client, error) { var client *apns2.Client - if cfg.Ios.Production { + if isProduction { client = apns2.NewClient(certificate).Production() } else { client = apns2.NewClient(certificate).Development() } - if cfg.Core.HTTPProxy == "" { - return client, nil - } - //nolint:gosec tlsConfig := &tls.Config{ Certificates: []tls.Certificate{certificate}, @@ -189,19 +185,15 @@ func newApnsClient(cfg *config.ConfYaml, certificate tls.Certificate) (*apns2.Cl return client, nil } -func newApnsTokenClient(cfg *config.ConfYaml, token *token.Token) (*apns2.Client, error) { +func newApnsTokenClient(token *token.Token, isProduction bool) (*apns2.Client, error) { var client *apns2.Client - if cfg.Ios.Production { + if isProduction { client = apns2.NewTokenClient(token).Production() } else { client = apns2.NewTokenClient(token).Development() } - if cfg.Core.HTTPProxy == "" { - return client, nil - } - transport := &http.Transport{ DialTLS: DialTLS(nil), Proxy: http.DefaultTransport.(*http.Transport).Proxy, @@ -386,14 +378,14 @@ func GetIOSNotification(req *PushNotification) *apns2.Notification { func getApnsClient(cfg *config.ConfYaml, req *PushNotification) (client *apns2.Client) { switch { case req.Production: - client = ApnsClient.Production() + client = ApnsClients[req.TenantId].Production() case req.Development: - client = ApnsClient.Development() + client = ApnsClients[req.TenantId].Development() default: - if cfg.Ios.Production { - client = ApnsClient.Production() + if cfg.Tenants[req.TenantId].Ios.Production { + client = ApnsClients[req.TenantId].Production() } else { - client = ApnsClient.Development() + client = ApnsClients[req.TenantId].Development() } } @@ -403,10 +395,14 @@ func getApnsClient(cfg *config.ConfYaml, req *PushNotification) (client *apns2.C // PushToIOS provide send notification to APNs server. func PushToIOS(req *PushNotification, cfg *config.ConfYaml) (resp *ResponsePush, err error) { logx.LogAccess.Debug("Start push notification for iOS") + if req.TenantId == "" { + logx.LogError.Error("missing tenant id for Android notification") + return + } var ( retryCount = 0 - maxRetry = cfg.Ios.MaxRetry + maxRetry = cfg.Tenants[req.TenantId].Ios.MaxRetry ) if req.Retry > 0 && req.Retry < maxRetry { @@ -422,12 +418,12 @@ Retry: client := getApnsClient(cfg, req) var wg sync.WaitGroup - for _, token := range req.Tokens { + for _, iosToken := range req.Tokens { // occupy push slot - MaxConcurrentIOSPushes <- struct{}{} + MaxConcurrentIOSPushes[req.TenantId] <- struct{}{} wg.Add(1) go func(notification apns2.Notification, token string) { - notification.DeviceToken = token + notification.DeviceToken = iosToken // send ios notification res, err := client.Push(¬ification) @@ -439,26 +435,26 @@ Retry: } // apns server error - errLog := logPush(cfg, core.FailedPush, token, req, err) + errLog := logPush(cfg, core.FailedPush, iosToken, req, err) resp.Logs = append(resp.Logs, errLog) status.StatStorage.AddIosError(1) // We should retry only "retryable" statuses. More info about response: // See https://apple.co/3AdNane (Handling Notification Responses from APNs) if res != nil && res.StatusCode >= http.StatusInternalServerError { - newTokens = append(newTokens, token) + newTokens = append(newTokens, iosToken) } } if res != nil && res.Sent() { - logPush(cfg, core.SucceededPush, token, req, nil) + logPush(cfg, core.SucceededPush, iosToken, req, nil) status.StatStorage.AddIosSuccess(1) } // free push slot - <-MaxConcurrentIOSPushes + <-MaxConcurrentIOSPushes[req.TenantId] wg.Done() - }(*notification, token) + }(*notification, iosToken) } wg.Wait() diff --git a/notify/notification_apns_test.go b/notify/notification_apns_test.go index ea2c5b588..fb32a5613 100644 --- a/notify/notification_apns_test.go +++ b/notify/notification_apns_test.go @@ -36,27 +36,33 @@ var ( func TestDisabledAndroidIosConf(t *testing.T) { cfg, _ := config.LoadConf() - cfg.Android.Enabled = false - cfg.Huawei.Enabled = false + var firstTenant *config.SectionTenant + for _, value := range cfg.Tenants { + firstTenant = value + break // exit the loop after the first iteration + } + firstTenant.Android.Enabled = false + firstTenant.Huawei.Enabled = false + firstTenant.Ios.Enabled = false err := CheckPushConf(cfg) assert.Error(t, err) - assert.Equal(t, "please enable iOS, Android or Huawei config in yml config", err.Error()) } func TestMissingIOSCertificate(t *testing.T) { cfg, _ := config.LoadConf() + var _, tenant = getFirstTenant(cfg) - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = "" - cfg.Ios.KeyBase64 = "" + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = "" + tenant.Ios.KeyBase64 = "" err := CheckPushConf(cfg) assert.Error(t, err) assert.Equal(t, "missing iOS certificate key", err.Error()) - cfg.Ios.KeyPath = "test.pem" + tenant.Ios.KeyPath = "test.pem" err = CheckPushConf(cfg) assert.Error(t, err) @@ -572,18 +578,19 @@ func TestIOSAlertNotificationStructure(t *testing.T) { func TestWrongIosCertificateExt(t *testing.T) { cfg, _ := config.LoadConf() + var tenantId, tenant = getFirstTenant(cfg) - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = "test" - err := InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = "test" + err := InitAPNSClient(tenantId, tenant) assert.Error(t, err) assert.Equal(t, "wrong certificate key extension", err.Error()) - cfg.Ios.KeyPath = "" - cfg.Ios.KeyBase64 = "abcd" - cfg.Ios.KeyType = "abcd" - err = InitAPNSClient(cfg) + tenant.Ios.KeyPath = "" + tenant.Ios.KeyBase64 = "abcd" + tenant.Ios.KeyType = "abcd" + err = InitAPNSClient(tenantId, tenant) assert.Error(t, err) assert.Equal(t, "wrong certificate key type", err.Error()) @@ -591,129 +598,134 @@ func TestWrongIosCertificateExt(t *testing.T) { func TestAPNSClientDevHost(t *testing.T) { cfg, _ := config.LoadConf() + var tenantId, tenant = getFirstTenant(cfg) - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = "../certificate/certificate-valid.p12" - err := InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = "../certificate/certificate-valid.p12" + err := InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host) + assert.Equal(t, apns2.HostDevelopment, ApnsClients[tenantId].Host) - cfg.Ios.KeyPath = "" - cfg.Ios.KeyBase64 = certificateValidP12 - cfg.Ios.KeyType = "p12" - err = InitAPNSClient(cfg) + tenant.Ios.KeyPath = "" + tenant.Ios.KeyBase64 = certificateValidP12 + tenant.Ios.KeyType = "p12" + err = InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host) + assert.Equal(t, apns2.HostDevelopment, ApnsClients[tenantId].Host) } func TestAPNSClientProdHost(t *testing.T) { cfg, _ := config.LoadConf() + var tenantId, tenant = getFirstTenant(cfg) - cfg.Ios.Enabled = true - cfg.Ios.Production = true - cfg.Ios.KeyPath = testKeyPath - err := InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.Production = true + tenant.Ios.KeyPath = testKeyPath + err := InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - assert.Equal(t, apns2.HostProduction, ApnsClient.Host) + assert.Equal(t, apns2.HostProduction, ApnsClients[tenantId].Host) - cfg.Ios.KeyPath = "" - cfg.Ios.KeyBase64 = certificateValidPEM - cfg.Ios.KeyType = "pem" - err = InitAPNSClient(cfg) + tenant.Ios.KeyPath = "" + tenant.Ios.KeyBase64 = certificateValidPEM + tenant.Ios.KeyType = "pem" + err = InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - assert.Equal(t, apns2.HostProduction, ApnsClient.Host) + assert.Equal(t, apns2.HostProduction, ApnsClients[tenantId].Host) } func TestAPNSClientInvaildToken(t *testing.T) { cfg, _ := config.LoadConf() + var tenantId, tenant = getFirstTenant(cfg) - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = "../certificate/authkey-invalid.p8" - err := InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = "../certificate/authkey-invalid.p8" + err := InitAPNSClient(tenantId, tenant) assert.Error(t, err) - cfg.Ios.KeyPath = "" - cfg.Ios.KeyBase64 = authkeyInvalidP8 - cfg.Ios.KeyType = "p8" - err = InitAPNSClient(cfg) + tenant.Ios.KeyPath = "" + tenant.Ios.KeyBase64 = authkeyInvalidP8 + tenant.Ios.KeyType = "p8" + err = InitAPNSClient(tenantId, tenant) assert.Error(t, err) // empty key-id or team-id - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = testKeyPathP8 - err = InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = testKeyPathP8 + err = InitAPNSClient(tenantId, tenant) assert.Error(t, err) - cfg.Ios.KeyID = "key-id" - cfg.Ios.TeamID = "" - err = InitAPNSClient(cfg) + tenant.Ios.KeyID = "key-id" + tenant.Ios.TeamID = "" + err = InitAPNSClient(tenantId, tenant) assert.Error(t, err) - cfg.Ios.KeyID = "" - cfg.Ios.TeamID = "team-id" - err = InitAPNSClient(cfg) + tenant.Ios.KeyID = "" + tenant.Ios.TeamID = "team-id" + err = InitAPNSClient(tenantId, tenant) assert.Error(t, err) } func TestAPNSClientVaildToken(t *testing.T) { cfg, _ := config.LoadConf() + var tenantId, tenant = getFirstTenant(cfg) - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = testKeyPathP8 - cfg.Ios.KeyID = "key-id" - cfg.Ios.TeamID = "team-id" - err := InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = testKeyPathP8 + tenant.Ios.KeyID = "key-id" + tenant.Ios.TeamID = "team-id" + err := InitAPNSClient(tenantId, tenant) assert.NoError(t, err) - assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host) + assert.Equal(t, apns2.HostDevelopment, ApnsClients[tenantId].Host) - cfg.Ios.Production = true - err = InitAPNSClient(cfg) + tenant.Ios.Production = true + err = InitAPNSClient(tenantId, tenant) assert.NoError(t, err) - assert.Equal(t, apns2.HostProduction, ApnsClient.Host) + assert.Equal(t, apns2.HostProduction, ApnsClients[tenantId].Host) // test base64 - cfg.Ios.Production = false - cfg.Ios.KeyPath = "" - cfg.Ios.KeyBase64 = authkeyValidP8 - cfg.Ios.KeyType = "p8" - err = InitAPNSClient(cfg) + tenant.Ios.Production = false + tenant.Ios.KeyPath = "" + tenant.Ios.KeyBase64 = authkeyValidP8 + tenant.Ios.KeyType = "p8" + err = InitAPNSClient(tenantId, tenant) assert.NoError(t, err) - assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host) + assert.Equal(t, apns2.HostDevelopment, ApnsClients[tenantId].Host) - cfg.Ios.Production = true - err = InitAPNSClient(cfg) + tenant.Ios.Production = true + err = InitAPNSClient(tenantId, tenant) assert.NoError(t, err) - assert.Equal(t, apns2.HostProduction, ApnsClient.Host) + assert.Equal(t, apns2.HostProduction, ApnsClients[tenantId].Host) } func TestAPNSClientUseProxy(t *testing.T) { cfg, _ := config.LoadConf() + var tenantId, tenant = getFirstTenant(cfg) - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = "../certificate/certificate-valid.p12" + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = "../certificate/certificate-valid.p12" cfg.Core.HTTPProxy = "http://127.0.0.1:8080" _ = SetProxy(cfg.Core.HTTPProxy) - err := InitAPNSClient(cfg) + err := InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host) + assert.Equal(t, apns2.HostDevelopment, ApnsClients[tenantId].Host) req, _ := http.NewRequestWithContext(context.Background(), "GET", apns2.HostDevelopment, nil) - actualProxyURL, err := ApnsClient.HTTPClient.Transport.(*http.Transport).Proxy(req) + actualProxyURL, err := ApnsClients[tenantId].HTTPClient.Transport.(*http.Transport).Proxy(req) assert.Nil(t, err) expectedProxyURL, _ := url.ParseRequestURI(cfg.Core.HTTPProxy) assert.Equal(t, expectedProxyURL, actualProxyURL) - cfg.Ios.KeyPath = testKeyPathP8 - cfg.Ios.TeamID = "example.team" - cfg.Ios.KeyID = "example.key" - err = InitAPNSClient(cfg) + tenant.Ios.KeyPath = testKeyPathP8 + tenant.Ios.TeamID = "example.team" + tenant.Ios.KeyID = "example.key" + err = InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host) - assert.NotNil(t, ApnsClient.Token) + assert.Equal(t, apns2.HostDevelopment, ApnsClients[tenantId].Host) + assert.NotNil(t, ApnsClients[tenantId].Token) req, _ = http.NewRequestWithContext(context.Background(), "GET", apns2.HostDevelopment, nil) - actualProxyURL, err = ApnsClient.HTTPClient.Transport.(*http.Transport).Proxy(req) + actualProxyURL, err = ApnsClients[tenantId].HTTPClient.Transport.(*http.Transport).Proxy(req) assert.Nil(t, err) expectedProxyURL, _ = url.ParseRequestURI(cfg.Core.HTTPProxy) @@ -724,11 +736,12 @@ func TestAPNSClientUseProxy(t *testing.T) { func TestPushToIOS(t *testing.T) { cfg, _ := config.LoadConf() - MaxConcurrentIOSPushes = make(chan struct{}, cfg.Ios.MaxConcurrentPushes) + var tenantId, tenant = getFirstTenant(cfg) + MaxConcurrentIOSPushes[tenantId] = make(chan struct{}, tenant.Ios.MaxConcurrentPushes) - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = testKeyPath - err := InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = testKeyPath + err := InitAPNSClient(tenantId, tenant) assert.Nil(t, err) err = status.InitAppStatus(cfg) assert.Nil(t, err) @@ -748,10 +761,11 @@ func TestPushToIOS(t *testing.T) { func TestApnsHostFromRequest(t *testing.T) { cfg, _ := config.LoadConf() + var tenantId, tenant = getFirstTenant(cfg) - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = testKeyPath - err := InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = testKeyPath + err := InitAPNSClient(tenantId, tenant) assert.Nil(t, err) err = status.InitAppStatus(cfg) assert.Nil(t, err) @@ -769,11 +783,22 @@ func TestApnsHostFromRequest(t *testing.T) { assert.Equal(t, apns2.HostDevelopment, client.Host) req = &PushNotification{} - cfg.Ios.Production = true + tenant.Ios.Production = true client = getApnsClient(cfg, req) assert.Equal(t, apns2.HostProduction, client.Host) - cfg.Ios.Production = false + tenant.Ios.Production = false client = getApnsClient(cfg, req) assert.Equal(t, apns2.HostDevelopment, client.Host) } + +func getFirstTenant(cfg *config.ConfYaml) (string, config.SectionTenant) { + var firstTenant config.SectionTenant + var firstTenantId string + for key, value := range cfg.Tenants { + firstTenantId = key + firstTenant = *value + break // exit the loop after the first iteration + } + return firstTenantId, firstTenant +} diff --git a/notify/notification_fcm.go b/notify/notification_fcm.go index 9c77f9ac1..fd7ce6d53 100644 --- a/notify/notification_fcm.go +++ b/notify/notification_fcm.go @@ -13,23 +13,19 @@ import ( ) // InitFCMClient use for initialize FCM Client. -func InitFCMClient(cfg *config.ConfYaml, key string) (*fcm.Client, error) { +func InitFCMClient(tenantId string, key string) (*fcm.Client, error) { var err error - if key == "" && cfg.Android.APIKey == "" { - return nil, errors.New("missing android api key") + if key == "" { + return nil, errors.New("missing Android API Key for tenant " + tenantId) } - if key != "" && key != cfg.Android.APIKey { - return fcm.NewClient(key) + if FCMClients[tenantId] == nil { + FCMClients[tenantId], err = fcm.NewClient(key) + return FCMClients[tenantId], err } - if FCMClient == nil { - FCMClient, err = fcm.NewClient(cfg.Android.APIKey) - return FCMClient, err - } - - return FCMClient, nil + return FCMClients[tenantId], nil } // GetAndroidNotification use for define Android notification. @@ -107,11 +103,15 @@ func GetAndroidNotification(req *PushNotification) *fcm.Message { // PushToAndroid provide send notification to Android server. func PushToAndroid(req *PushNotification, cfg *config.ConfYaml) (resp *ResponsePush, err error) { logx.LogAccess.Debug("Start push notification for Android") + if req.TenantId == "" { + logx.LogError.Error("missing tenant id for Android notification") + return + } var ( client *fcm.Client retryCount = 0 - maxRetry = cfg.Android.MaxRetry + maxRetry = cfg.Tenants[req.TenantId].Android.MaxRetry ) if req.Retry > 0 && req.Retry < maxRetry { @@ -131,9 +131,9 @@ Retry: notification := GetAndroidNotification(req) if req.APIKey != "" { - client, err = InitFCMClient(cfg, req.APIKey) + client, err = InitFCMClient(req.TenantId, req.APIKey) } else { - client, err = InitFCMClient(cfg, cfg.Android.APIKey) + client, err = InitFCMClient(req.TenantId, cfg.Tenants[req.TenantId].Android.APIKey) } if err != nil { @@ -235,6 +235,7 @@ Retry: func logPush(cfg *config.ConfYaml, status, token string, req *PushNotification, err error) logx.LogPushEntry { return logx.LogPush(&logx.InputLog{ ID: req.ID, + TenantId: req.TenantId, Status: status, Token: token, Message: req.Message, diff --git a/notify/notification_fcm_test.go b/notify/notification_fcm_test.go index 04a4bb71d..c6a999ead 100644 --- a/notify/notification_fcm_test.go +++ b/notify/notification_fcm_test.go @@ -13,33 +13,35 @@ import ( func TestMissingAndroidAPIKey(t *testing.T) { cfg, _ := config.LoadConf() + tenant := cfg.Tenants[tenantId] - cfg.Android.Enabled = true - cfg.Android.APIKey = "" + tenant.Android.Enabled = true + tenant.Android.APIKey = "" err := CheckPushConf(cfg) assert.Error(t, err) - assert.Equal(t, "missing android api key", err.Error()) + assert.Equal(t, "missing Android API Key for tenant "+tenantId, err.Error()) } func TestMissingKeyForInitFCMClient(t *testing.T) { - cfg, _ := config.LoadConf() - cfg.Android.APIKey = "" - client, err := InitFCMClient(cfg, "") + tenantId := "1" + client, err := InitFCMClient(tenantId, "") assert.Nil(t, client) assert.Error(t, err) - assert.Equal(t, "missing android api key", err.Error()) + assert.Equal(t, "missing Android API Key for tenant "+tenantId, err.Error()) } func TestPushToAndroidWrongToken(t *testing.T) { cfg, _ := config.LoadConf() + tenant := cfg.Tenants[tenantId] - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") req := &PushNotification{ + TenantId: tenantId, Tokens: []string{"aaaaaa", "bbbbb"}, Platform: core.PlatFormAndroid, Message: "Welcome", @@ -53,15 +55,17 @@ func TestPushToAndroidWrongToken(t *testing.T) { func TestPushToAndroidRightTokenForJSONLog(t *testing.T) { cfg, _ := config.LoadConf() + tenant := cfg.Tenants[tenantId] - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") // log for json cfg.Log.Format = "json" androidToken := os.Getenv("ANDROID_TEST_TOKEN") req := &PushNotification{ + TenantId: tenantId, Tokens: []string{androidToken}, Platform: core.PlatFormAndroid, Message: "Welcome", @@ -74,13 +78,15 @@ func TestPushToAndroidRightTokenForJSONLog(t *testing.T) { func TestPushToAndroidRightTokenForStringLog(t *testing.T) { cfg, _ := config.LoadConf() + tenant := cfg.Tenants[tenantId] - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") androidToken := os.Getenv("ANDROID_TEST_TOKEN") req := &PushNotification{ + TenantId: tenantId, Tokens: []string{androidToken}, Platform: core.PlatFormAndroid, Message: "Welcome", @@ -93,14 +99,16 @@ func TestPushToAndroidRightTokenForStringLog(t *testing.T) { func TestOverwriteAndroidAPIKey(t *testing.T) { cfg, _ := config.LoadConf() + tenant := cfg.Tenants[tenantId] cfg.Core.Sync = true - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") androidToken := os.Getenv("ANDROID_TEST_TOKEN") req := &PushNotification{ + TenantId: tenantId, Tokens: []string{androidToken, "bbbbb"}, Platform: core.PlatFormAndroid, Message: "Welcome", @@ -120,8 +128,9 @@ func TestFCMMessage(t *testing.T) { // the message must specify at least one registration ID req := &PushNotification{ - Message: "Test", - Tokens: []string{}, + TenantId: "test", + Message: "Test", + Tokens: []string{}, } err = CheckMessage(req) @@ -129,8 +138,9 @@ func TestFCMMessage(t *testing.T) { // the token must not be empty req = &PushNotification{ - Message: "Test", - Tokens: []string{""}, + TenantId: "test", + Message: "Test", + Tokens: []string{""}, } err = CheckMessage(req) @@ -138,6 +148,7 @@ func TestFCMMessage(t *testing.T) { // ignore check token length if send topic message req = &PushNotification{ + TenantId: "test", Message: "Test", Platform: core.PlatFormAndroid, To: "/topics/foo-bar", @@ -148,6 +159,7 @@ func TestFCMMessage(t *testing.T) { // "condition": "'dogs' in topics || 'cats' in topics", req = &PushNotification{ + TenantId: "test", Message: "Test", Platform: core.PlatFormAndroid, Condition: "'dogs' in topics || 'cats' in topics", @@ -158,6 +170,7 @@ func TestFCMMessage(t *testing.T) { // the message may specify at most 1000 registration IDs req = &PushNotification{ + TenantId: "test", Message: "Test", Platform: core.PlatFormAndroid, Tokens: make([]string, 1001), @@ -170,6 +183,7 @@ func TestFCMMessage(t *testing.T) { // between 0 and 2419200 (4 weeks) timeToLive := uint(2419201) req = &PushNotification{ + TenantId: "test", Message: "Test", Platform: core.PlatFormAndroid, Tokens: []string{"XXXXXXXXX"}, @@ -182,6 +196,7 @@ func TestFCMMessage(t *testing.T) { // Pass timeToLive = uint(86400) req = &PushNotification{ + TenantId: "test", Message: "Test", Platform: core.PlatFormAndroid, Tokens: []string{"XXXXXXXXX"}, @@ -194,12 +209,14 @@ func TestFCMMessage(t *testing.T) { func TestCheckAndroidMessage(t *testing.T) { cfg, _ := config.LoadConf() + tenant := cfg.Tenants[tenantId] - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") timeToLive := uint(2419201) req := &PushNotification{ + TenantId: tenantId, Tokens: []string{"aaaaaa", "bbbbb"}, Platform: core.PlatFormAndroid, Message: "Welcome", diff --git a/notify/notification_hms.go b/notify/notification_hms.go index dbb2a4b9f..12c78bc59 100644 --- a/notify/notification_hms.go +++ b/notify/notification_hms.go @@ -36,7 +36,7 @@ func GetPushClient(conf *c.Config) (*client.HMSClient, error) { } // InitHMSClient use for initialize HMS Client. -func InitHMSClient(cfg *config.ConfYaml, appSecret, appID string) (*client.HMSClient, error) { +func InitHMSClient(cfg *config.ConfYaml, tenantId string, appSecret, appID string) (*client.HMSClient, error) { if appSecret == "" { return nil, errors.New("missing huawei app secret") } @@ -52,15 +52,15 @@ func InitHMSClient(cfg *config.ConfYaml, appSecret, appID string) (*client.HMSCl PushUrl: "https://push-api.cloud.huawei.com", } - if appSecret != cfg.Huawei.AppSecret || appID != cfg.Huawei.AppID { + if appSecret != cfg.Tenants[tenantId].Huawei.APIKey || appID != cfg.Tenants[tenantId].Huawei.APPId { return GetPushClient(conf) } - if HMSClient == nil { + if HMSClients[tenantId] == nil { return GetPushClient(conf) } - return HMSClient, nil + return HMSClients[tenantId], nil } // GetHuaweiNotification use for define HMS notification. @@ -168,7 +168,7 @@ func PushToHuawei(req *PushNotification, cfg *config.ConfYaml) (resp *ResponsePu var ( client *client.HMSClient retryCount = 0 - maxRetry = cfg.Huawei.MaxRetry + maxRetry = cfg.Tenants[req.TenantId].Huawei.MaxRetry ) if req.Retry > 0 && req.Retry < maxRetry { @@ -182,7 +182,7 @@ func PushToHuawei(req *PushNotification, cfg *config.ConfYaml) (resp *ResponsePu return } - client, err = InitHMSClient(cfg, cfg.Huawei.AppSecret, cfg.Huawei.AppID) + client, err = InitHMSClient(cfg, req.TenantId, cfg.Tenants[req.TenantId].Huawei.APIKey, cfg.Tenants[req.TenantId].Huawei.APPId) if err != nil { // HMS server error diff --git a/notify/notification_hms_test.go b/notify/notification_hms_test.go index eda2114af..3e0d5c1e3 100644 --- a/notify/notification_hms_test.go +++ b/notify/notification_hms_test.go @@ -10,9 +10,11 @@ import ( func TestMissingHuaweiAppSecret(t *testing.T) { cfg, _ := config.LoadConf() + tenantId := "tenant_id1" + tenant := cfg.Tenants[tenantId] - cfg.Huawei.Enabled = true - cfg.Huawei.AppSecret = "" + tenant.Huawei.Enabled = true + tenant.Huawei.APIKey = "" err := CheckPushConf(cfg) @@ -22,9 +24,11 @@ func TestMissingHuaweiAppSecret(t *testing.T) { func TestMissingHuaweiAppID(t *testing.T) { cfg, _ := config.LoadConf() + tenantId := "tenant_id1" + tenant := cfg.Tenants[tenantId] - cfg.Huawei.Enabled = true - cfg.Huawei.AppID = "" + tenant.Huawei.Enabled = true + tenant.Huawei.APPId = "" err := CheckPushConf(cfg) @@ -34,7 +38,7 @@ func TestMissingHuaweiAppID(t *testing.T) { func TestMissingAppSecretForInitHMSClient(t *testing.T) { cfg, _ := config.LoadConf() - client, err := InitHMSClient(cfg, "", "APP_SECRET") + client, err := InitHMSClient(cfg, "", "APP_SECRET", "") assert.Nil(t, client) assert.Error(t, err) @@ -43,7 +47,7 @@ func TestMissingAppSecretForInitHMSClient(t *testing.T) { func TestMissingAppIDForInitHMSClient(t *testing.T) { cfg, _ := config.LoadConf() - client, err := InitHMSClient(cfg, "APP_ID", "") + client, err := InitHMSClient(cfg, "tenant_id1", "APP_ID", "") assert.Nil(t, client) assert.Error(t, err) diff --git a/notify/notification_test.go b/notify/notification_test.go index c135eab53..f6ecf3dcf 100644 --- a/notify/notification_test.go +++ b/notify/notification_test.go @@ -8,14 +8,19 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + tenantId = "legacy" +) + func TestCorrectConf(t *testing.T) { cfg, _ := config.LoadConf() + tenant := cfg.Tenants[tenantId] - cfg.Android.Enabled = true - cfg.Android.APIKey = "xxxxx" + tenant.Android.Enabled = true + tenant.Android.APIKey = "xxxxx" - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = testKeyPath + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = testKeyPath err := CheckPushConf(cfg) diff --git a/router/server.go b/router/server.go index 17e9fb042..94e5bf24c 100644 --- a/router/server.go +++ b/router/server.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "os" + "path/filepath" "sync" "github.com/appleboy/gorush/config" @@ -58,6 +59,8 @@ func versionHandler(c *gin.Context) { func pushHandler(cfg *config.ConfYaml, q *queue.Queue) gin.HandlerFunc { return func(c *gin.Context) { + fullPath := c.FullPath() + tenantId := filepath.Base(fullPath) var form notify.RequestPush var msg string @@ -96,7 +99,7 @@ func pushHandler(cfg *config.ConfYaml, q *queue.Queue) gin.HandlerFunc { } }() - counts, logs := handleNotification(ctx, cfg, form, q) + counts, logs := handleNotification(ctx, cfg, form, q, tenantId) c.JSON(http.StatusOK, gin.H{ "success": "ok", @@ -212,13 +215,15 @@ func routerEngine(cfg *config.ConfYaml, q *queue.Queue) *gin.Engine { r.GET(cfg.API.StatAppURI, appStatusHandler(q)) r.GET(cfg.API.ConfigURI, configHandler(cfg)) r.GET(cfg.API.SysStatURI, sysStatsHandler()) - r.POST(cfg.API.PushURI, pushHandler(cfg, q)) r.GET(cfg.API.MetricURI, metricsHandler) r.GET(cfg.API.HealthURI, heartbeatHandler) r.HEAD(cfg.API.HealthURI, heartbeatHandler) r.GET("/version", versionHandler) r.GET("/", rootHandler) + for _, tenant := range cfg.Tenants { + r.POST(tenant.PushURI, pushHandler(cfg, q)) + } return r } @@ -233,6 +238,7 @@ func markFailedNotification( for _, token := range notification.Tokens { logs = append(logs, logx.GetLogPushEntry(&logx.InputLog{ ID: notification.ID, + TenantId: notification.TenantId, Status: core.FailedPush, Token: token, Message: notification.Message, @@ -252,10 +258,11 @@ func handleNotification( cfg *config.ConfYaml, req notify.RequestPush, q *queue.Queue, + tenantId string, ) (int, []logx.LogPushEntry) { var count int wg := sync.WaitGroup{} - newNotification := []*notify.PushNotification{} + var newNotification []*notify.PushNotification if cfg.Core.Sync && !core.IsLocalQueue(core.Queue(cfg.Queue.Engine)) { cfg.Core.Sync = false @@ -263,20 +270,22 @@ func handleNotification( for i := range req.Notifications { notification := &req.Notifications[i] + tenant := cfg.Tenants[tenantId] switch notification.Platform { case core.PlatFormIos: - if !cfg.Ios.Enabled { + if !tenant.Ios.Enabled { continue } case core.PlatFormAndroid: - if !cfg.Android.Enabled { + if !tenant.Android.Enabled { continue } case core.PlatFormHuawei: - if !cfg.Huawei.Enabled { + if !tenant.Huawei.Enabled { continue } } + notification.TenantId = tenantId newNotification = append(newNotification, notification) } diff --git a/router/server_test.go b/router/server_test.go index 845b65bdf..08934dea5 100644 --- a/router/server_test.go +++ b/router/server_test.go @@ -31,16 +31,20 @@ var ( testKeyPath = "../certificate/certificate-valid.pem" ) +const ( + tenantId = "legacy" +) + func TestMain(m *testing.M) { cfg := initTest() if err := status.InitAppStatus(cfg); err != nil { log.Fatal(err) } - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + cfg.Tenants[tenantId].Android.Enabled = true + cfg.Tenants[tenantId].Android.APIKey = os.Getenv("ANDROID_API_KEY") - if _, err := notify.InitFCMClient(cfg, ""); err != nil { + if _, err := notify.InitFCMClient(tenantId, ""); err != nil { log.Fatal(err) } @@ -376,8 +380,8 @@ func TestSuccessPushHandler(t *testing.T) { t.Skip() cfg := initTest() - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + cfg.Tenants[tenantId].Android.Enabled = true + cfg.Tenants[tenantId].Android.APIKey = os.Getenv("ANDROID_API_KEY") androidToken := os.Getenv("ANDROID_TEST_TOKEN") @@ -472,13 +476,14 @@ func TestSenMultipleNotifications(t *testing.T) { ctx := context.Background() cfg := initTest() - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = testKeyPath - err := notify.InitAPNSClient(cfg) + tenant := *cfg.Tenants[tenantId] + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = testKeyPath + err := notify.InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") androidToken := os.Getenv("ANDROID_TEST_TOKEN") @@ -499,7 +504,7 @@ func TestSenMultipleNotifications(t *testing.T) { }, } - count, logs := handleNotification(ctx, cfg, req, q) + count, logs := handleNotification(ctx, cfg, req, q, tenantId) assert.Equal(t, 3, count) assert.Equal(t, 0, len(logs)) } @@ -507,14 +512,15 @@ func TestSenMultipleNotifications(t *testing.T) { func TestDisabledAndroidNotifications(t *testing.T) { ctx := context.Background() cfg := initTest() + tenant := *cfg.Tenants[tenantId] - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = testKeyPath - err := notify.InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = testKeyPath + err := notify.InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - cfg.Android.Enabled = false - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = false + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") androidToken := os.Getenv("ANDROID_TEST_TOKEN") @@ -535,7 +541,7 @@ func TestDisabledAndroidNotifications(t *testing.T) { }, } - count, logs := handleNotification(ctx, cfg, req, q) + count, logs := handleNotification(ctx, cfg, req, q, tenantId) assert.Equal(t, 1, count) assert.Equal(t, 0, len(logs)) } @@ -543,14 +549,15 @@ func TestDisabledAndroidNotifications(t *testing.T) { func TestSyncModeForNotifications(t *testing.T) { ctx := context.Background() cfg := initTest() + tenant := *cfg.Tenants[tenantId] - cfg.Ios.Enabled = true - cfg.Ios.KeyPath = testKeyPath - err := notify.InitAPNSClient(cfg) + tenant.Ios.Enabled = true + tenant.Ios.KeyPath = testKeyPath + err := notify.InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") // enable sync mode cfg.Core.Sync = true @@ -576,7 +583,7 @@ func TestSyncModeForNotifications(t *testing.T) { }, } - count, logs := handleNotification(ctx, cfg, req, q) + count, logs := handleNotification(ctx, cfg, req, q, tenantId) assert.Equal(t, 3, count) assert.Equal(t, 2, len(logs)) } @@ -584,9 +591,10 @@ func TestSyncModeForNotifications(t *testing.T) { func TestSyncModeForTopicNotification(t *testing.T) { ctx := context.Background() cfg := initTest() + tenant := *cfg.Tenants[tenantId] - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") cfg.Log.HideToken = false // enable sync mode @@ -619,7 +627,7 @@ func TestSyncModeForTopicNotification(t *testing.T) { }, } - count, logs := handleNotification(ctx, cfg, req, q) + count, logs := handleNotification(ctx, cfg, req, q, tenantId) assert.Equal(t, 2, count) assert.Equal(t, 1, len(logs)) } @@ -627,9 +635,10 @@ func TestSyncModeForTopicNotification(t *testing.T) { func TestSyncModeForDeviceGroupNotification(t *testing.T) { ctx := context.Background() cfg := initTest() + tenant := *cfg.Tenants[tenantId] - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") cfg.Log.HideToken = false // enable sync mode @@ -646,7 +655,7 @@ func TestSyncModeForDeviceGroupNotification(t *testing.T) { }, } - count, logs := handleNotification(ctx, cfg, req, q) + count, logs := handleNotification(ctx, cfg, req, q, tenantId) assert.Equal(t, 1, count) assert.Equal(t, 1, len(logs)) } @@ -654,14 +663,15 @@ func TestSyncModeForDeviceGroupNotification(t *testing.T) { func TestDisabledIosNotifications(t *testing.T) { ctx := context.Background() cfg := initTest() + tenant := *cfg.Tenants[tenantId] - cfg.Ios.Enabled = false - cfg.Ios.KeyPath = testKeyPath - err := notify.InitAPNSClient(cfg) + tenant.Ios.Enabled = false + tenant.Ios.KeyPath = testKeyPath + err := notify.InitAPNSClient(tenantId, tenant) assert.Nil(t, err) - cfg.Android.Enabled = true - cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY") + tenant.Android.Enabled = true + tenant.Android.APIKey = os.Getenv("ANDROID_API_KEY") androidToken := os.Getenv("ANDROID_TEST_TOKEN") @@ -682,7 +692,7 @@ func TestDisabledIosNotifications(t *testing.T) { }, } - count, logs := handleNotification(ctx, cfg, req, q) + count, logs := handleNotification(ctx, cfg, req, q, tenantId) assert.Equal(t, 2, count) assert.Equal(t, 0, len(logs)) } diff --git a/rpc/server.go b/rpc/server.go index 894bc3e0f..272ff3bcd 100644 --- a/rpc/server.go +++ b/rpc/server.go @@ -50,9 +50,9 @@ func (s *Server) Check(ctx context.Context, in *proto.HealthCheckRequest) (*prot Status: proto.HealthCheckResponse_SERVING, }, nil } - if status, ok := s.statusMap[in.Service]; ok { + if serverStatus, ok := s.statusMap[in.Service]; ok { return &proto.HealthCheckResponse{ - Status: status, + Status: serverStatus, }, nil } return nil, status.Error(codes.NotFound, "unknown service") @@ -62,7 +62,7 @@ func (s *Server) Check(ctx context.Context, in *proto.HealthCheckRequest) (*prot func (s *Server) Send(ctx context.Context, in *proto.NotificationRequest) (*proto.NotificationReply, error) { badge := int(in.Badge) notification := notify.PushNotification{ - ID: in.ID, + TenantId: "sample", Platform: int(in.Platform), Tokens: in.Tokens, Message: in.Message, diff --git a/tests/README.md b/tests/README.md index 01d860a43..eb1a9f160 100644 --- a/tests/README.md +++ b/tests/README.md @@ -14,12 +14,18 @@ see the JSON format: { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 1, "message": "Hello World iOS!" }, { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 2, "message": "Hello World Android!" } diff --git a/tests/test.json b/tests/test.json index c88a7c41b..164d7426b 100644 --- a/tests/test.json +++ b/tests/test.json @@ -1,13 +1,19 @@ { "notifications": [ { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 1, "message": "Hello World iOS!", "title": "Gorush with iOS" }, { - "tokens": ["token_a", "token_b"], + "tokens": [ + "token_a", + "token_b" + ], "platform": 2, "message": "Hello World Android!", "title": "Gorush with Android"