From fef84669c549b9d4e5985055ba67f1c868b52bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Thu, 8 Feb 2024 14:58:45 +0100 Subject: [PATCH] Fix #443: Add support for Huawei Mobile Services (#750) * Fix #443: Add support for Huawei Mobile Services --- docs/Configuration-Properties.md | 18 +- docs/Migration-Instructions.md | 1 + docs/PowerAuth-Push-Server-1.7.0.md | 11 ++ docs/Push-Server-API.md | 109 +++++++++++- docs/Push-Server-Administration.md | 37 +++- .../20240119-push_app_credentials-hms.xml | 23 +++ .../1.7.x/db.changelog-version.xml | 8 + .../db.changelog-module.xml | 1 + pom.xml | 8 + .../getlime/push/client/PushServerClient.java | 40 ++++- .../model/entity/PushMessageSendResult.java | 7 + .../model/entity/PushServerApplication.java | 52 ++---- .../model/enumeration/MobilePlatform.java | 8 +- .../CreateDeviceForActivationsRequest.java | 4 +- .../model/request/CreateDeviceRequest.java | 4 +- .../request/GetApplicationDetailRequest.java | 15 +- .../model/request/RemoveHuaweiRequest.java | 53 ++++++ .../model/request/UpdateHuaweiRequest.java | 60 +++++++ .../GetApplicationDetailResponse.java | 11 +- powerauth-push-server/pom.xml | 4 + .../PushServiceConfiguration.java | 67 +++++++ .../rest/AdministrationController.java | 70 +++++++- .../model/AppCredentialsEntity.java | 20 ++- .../push/repository/model/Platform.java | 7 +- .../repository/model/PlatformConverter.java | 2 + .../push/service/AppRelatedPushClient.java | 51 +----- .../service/DeviceRegistrationService.java | 1 + .../service/PushMessageSenderService.java | 20 ++- .../push/service/PushSendingWorker.java | 111 ++++++++++++ .../getlime/push/service/hms/HmsClient.java | 156 ++++++++++++++++ .../push/service/hms/HmsSendResponse.java | 25 +++ .../service/hms/request/AndroidConfig.java | 53 ++++++ .../hms/request/AndroidNotification.java | 143 +++++++++++++++ .../push/service/hms/request/ApnsConfig.java | 45 +++++ .../push/service/hms/request/ApnsHeaders.java | 50 ++++++ .../service/hms/request/ApnsHmsOptions.java | 36 ++++ .../hms/request/BadgeNotification.java | 42 +++++ .../push/service/hms/request/Button.java | 45 +++++ .../push/service/hms/request/ClickAction.java | 44 +++++ .../push/service/hms/request/Color.java | 45 +++++ .../service/hms/request/LightSettings.java | 41 +++++ .../push/service/hms/request/Message.java | 56 ++++++ .../service/hms/request/Notification.java | 37 ++++ .../push/service/hms/request/WebActions.java | 38 ++++ .../service/hms/request/WebHmsOptions.java | 33 ++++ .../service/hms/request/WebNotification.java | 68 +++++++ .../service/hms/request/WebPushConfig.java | 41 +++++ .../service/hms/request/WebpushHeaders.java | 38 ++++ .../src/main/resources/application.properties | 13 ++ .../rest/AdministrationControllerTest.java | 166 ++++++++++++++++++ .../push/service/hms/HmsClientTest.java | 157 +++++++++++++++++ .../application-external-service.properties | 3 + 52 files changed, 2072 insertions(+), 126 deletions(-) create mode 100644 docs/PowerAuth-Push-Server-1.7.0.md create mode 100644 docs/db/changelog/changesets/powerauth-push-server/1.7.x/20240119-push_app_credentials-hms.xml create mode 100644 docs/db/changelog/changesets/powerauth-push-server/1.7.x/db.changelog-version.xml create mode 100644 powerauth-push-model/src/main/java/io/getlime/push/model/request/RemoveHuaweiRequest.java create mode 100644 powerauth-push-model/src/main/java/io/getlime/push/model/request/UpdateHuaweiRequest.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/HmsClient.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/HmsSendResponse.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/AndroidConfig.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/AndroidNotification.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/ApnsConfig.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/ApnsHeaders.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/ApnsHmsOptions.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/BadgeNotification.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/Button.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/ClickAction.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/Color.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/LightSettings.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/Message.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/Notification.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/WebActions.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/WebHmsOptions.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/WebNotification.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/WebPushConfig.java create mode 100644 powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/WebpushHeaders.java create mode 100644 powerauth-push-server/src/test/java/io/getlime/push/controller/rest/AdministrationControllerTest.java create mode 100644 powerauth-push-server/src/test/java/io/getlime/push/service/hms/HmsClientTest.java create mode 100644 powerauth-push-server/src/test/resources/application-external-service.properties diff --git a/docs/Configuration-Properties.md b/docs/Configuration-Properties.md index 3a1f8eb1d..abf29072c 100644 --- a/docs/Configuration-Properties.md +++ b/docs/Configuration-Properties.md @@ -59,7 +59,7 @@ The Push Server uses the following public configuration properties: | `powerauth.push.service.apns.idlePingInterval` | `60000` | Interval in milliseconds specifying the frequency of APNS ping calls in idle state | | `powerauth.push.service.apns.concurrentConnections` | `1` | Push message concurrency settings | -# FCM Configuration +## FCM Configuration | Property | Default | Note | |---|---|---| @@ -72,6 +72,22 @@ The Push Server uses the following public configuration properties: | `powerauth.push.service.fcm.sendMessageUrl` | `https://fcm.googleapis.com/v1/projects/%s/messages:send` | Default URL for the FCM service | | `powerauth.push.service.fcm.connect.timeout` | `5000` | Push message gateway connect timeout in milliseconds | +## HMS Configuration + +| Property | Default | Note | +|---------------------------------------------------|---------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `powerauth.push.service.hms.proxy.enabled` | `false` | Flag indicating if the communication needs to go through proxy. | +| `powerauth.push.service.hms.proxy.host` | `127.0.0.1` | Proxy host. | +| `powerauth.push.service.hms.proxy.port` | `8080` | Proxy port. | +| `powerauth.push.service.hms.proxy.username` | `_empty_` | Proxy username. | +| `powerauth.push.service.hms.proxy.password` | `_empty_` | Proxy password. | +| `powerauth.push.service.hms.dataNotificationOnly` | `false` | Flag indicating that HMS service should never use "notification" format, only a data format with extra payload representing the notification. | +| `powerauth.push.service.hms.sendMessageUrl` | `https://push-api.cloud.huawei.com/v2/%s/messages:send` | Default URL for the HMS service. | +| `powerauth.push.service.hms.tokenUrl` | `https://oauth-login.cloud.huawei.com/oauth2/v3/token` | Default URL for the HMS OAuth service to obtain an access token. | +| `powerauth.push.service.hms.connect.timeout` | `5s` | Push message gateway connect timeout. | +| `powerauth.push.service.hms.response.timeout` | `60s` | Push message gateway maximum duration allowed between each network-level read operations. | +| `powerauth.push.service.hms.max-idle-time` | `200s` | Push message gateway ConnectionProvider max idle time. | + ## Correlation HTTP Header Configuration | Property | Default | Note | diff --git a/docs/Migration-Instructions.md b/docs/Migration-Instructions.md index 403c3dc30..4a5193988 100644 --- a/docs/Migration-Instructions.md +++ b/docs/Migration-Instructions.md @@ -2,6 +2,7 @@ This page contains PowerAuth Push Server migration instructions. +- [PowerAuth Push Server 1.7.0](./PowerAuth-Push-Server-1.7.0.md) - [PowerAuth Push Server 1.6.0](./PowerAuth-Push-Server-1.6.0.md) - [PowerAuth Push Server 1.5.0](./PowerAuth-Push-Server-1.5.0.md) - [PowerAuth Push Server 1.4.0](./PowerAuth-Push-Server-1.4.0.md) diff --git a/docs/PowerAuth-Push-Server-1.7.0.md b/docs/PowerAuth-Push-Server-1.7.0.md new file mode 100644 index 000000000..34b70620e --- /dev/null +++ b/docs/PowerAuth-Push-Server-1.7.0.md @@ -0,0 +1,11 @@ +# Migration from 1.6.x to 1.7.x + +This guide contains instructions for migration from PowerAuth Push Server version `1.6.x` to version `1.7.x`. + + +## Database Changes + + +### Huawei Mobile Services + +To support HMS, the columns `hms_project_id`, `hms_client_id`, and `hms_client_secret` have been added into the table `push_app_credentials`. diff --git a/docs/Push-Server-API.md b/docs/Push-Server-API.md index 8b2ea95e1..fb2418b15 100644 --- a/docs/Push-Server-API.md +++ b/docs/Push-Server-API.md @@ -143,7 +143,8 @@ Send a system status response, with basic information about the running applicat ## Device -Represents mobile device with iOS or Android that is capable to receive a push notification. Device has to first register with APNS or FCM to obtain push token. +Represents mobile device with iOS, Android or Huawei that is capable to receive a push notification. +Device has to first register with APNS, FCM, or HMS to obtain push token. Then it has to forward the push token to the push server end-point. After that push server is able to send push notification to the device. @@ -181,7 +182,7 @@ _Note: Since this endpoint is usually called by the back-end service, it is not - `appId` - Application that device is using. - `token` - Identifier for device. -- `platform` - `ios`, `android` +- `platform` - `ios`, `android`, `huawei` - `activationId` - Activation identifier #### Response 200 @@ -231,7 +232,7 @@ _Note: Since this endpoint is usually called by the back-end service, it is not - `appId` - Application that device is using. - `token` - Identifier for device. -- `platform` - `ios`, `android` +- `platform` - `ios`, `android`, `huawei` - `activationIds` - Associated activation identifiers #### Response 200 @@ -412,6 +413,12 @@ Send a single push message to given user via provided application, optionally to "pending": 0, "failed": 0, "total": 1 + }, + "huawei": { + "sent": 1, + "pending": 0, + "failed": 0, + "total": 1 } } } @@ -533,6 +540,12 @@ Sends a message batch - each item in the batch represents a message to given use "pending": 0, "failed": 0, "total": 1 + }, + "huawei": { + "sent": 1, + "pending": 0, + "failed": 0, + "total": 1 } } } @@ -1108,7 +1121,8 @@ Get list of all applications. { "appId": "mobile-app", "ios": true, - "android": true + "android": true, + "huawei": true } ] } @@ -1146,7 +1160,8 @@ Get list of applications which have not been configured yet. { "appId": "mobile-app-other", "ios": null, - "android": null + "android": null, + "huawei": null } ] } @@ -1193,12 +1208,14 @@ Get detail of an application. "application": { "appId": "mobile-app", "ios": true, - "android": true + "android": true, + "huawai": true }, "iosBundle": "some.bundle.id", "iosKeyId": "KEYID123456", "iosTeamId": "TEAMID123456", - "androidProjectId": "PROJECTID123" + "androidProjectId": "PROJECTID123", + "huaweiProjectId": "HMS123" } } ``` @@ -1405,6 +1422,84 @@ Remove FCM configuration for Android push messages. ``` + +### Update Huawei Configuration + +Update an Huawei configuration. + + +#### Request + + + + + + + + + + + +
MethodPOST / PUT
Resource URI/admin/app/huawei/update
+ + +```json +{ + "requestObject": { + "appId": "mobile-app", + "projectId": "PROJECTID123", + "clientId": "oAuth 2.0 client ID", + "clientSecret": "oAuth 2.0 client secret" + } +} +``` + +#### Response 200 + +```json +{ + "status": "OK" +} +``` + + + +### Remove Huawei Configuration + +Remove configuration for Huawei push messages. + +#### Request + + + + + + + + + + + +
MethodPOST / DELETE
Resource URI/admin/app/huawei/remove
+ + +```json +{ + "requestObject": { + "appId": "mobile-app" + } +} +``` + +#### Response 200 + +```json +{ + "status": "OK" +} +``` + + ## Message Inbox When communicating with your users, you can use the message inbox to store messages for users. Inbox is not responsible diff --git a/docs/Push-Server-Administration.md b/docs/Push-Server-Administration.md index 1ad83c2df..ee3ff306d 100644 --- a/docs/Push-Server-Administration.md +++ b/docs/Push-Server-Administration.md @@ -53,7 +53,8 @@ curl --request POST \ "requestObject": { "appId": 1, "includeIos": true, - "includeAndroid": true + "includeAndroid": true, + "includeHuawei": true } }' ``` @@ -136,7 +137,39 @@ curl --request DELETE \ }' ``` -Set the `appId` value for the Push Server application ID you want to update. +Set the `appId` value for the Push Server application ID you want to delete. + + +### Update Huawei Configuration + +```sh +curl --request POST \ + --url http://localhost:8080/powerauth-push-server/admin/app/huawei/update \ + --json '{ + "requestObject": { + "appId": 1, + "projectId": "projectId", + "clientId": "oAuth 2.0 client ID", + "clientSecret": "oAuth 2.0 client secret" + } +}' +``` + +Set the `appId` value for Push Server application ID to want to update. + +### Remove Huawei Configuration + +```sh +curl --request DELETE \ + --url http://localhost:8080/powerauth-push-server/admin/app/huawei/remove \ + --json '{ + "requestObject": { + "appId": 1 + } +}' +``` + +Set the `appId` value for the Push Server application ID you want to delete. ## Administration using SQL Database diff --git a/docs/db/changelog/changesets/powerauth-push-server/1.7.x/20240119-push_app_credentials-hms.xml b/docs/db/changelog/changesets/powerauth-push-server/1.7.x/20240119-push_app_credentials-hms.xml new file mode 100644 index 000000000..9a1918fea --- /dev/null +++ b/docs/db/changelog/changesets/powerauth-push-server/1.7.x/20240119-push_app_credentials-hms.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + Add hms_project_id, hms_client_id, and hms_client_secret columns to push_app_credentials + + + + + + + + + diff --git a/docs/db/changelog/changesets/powerauth-push-server/1.7.x/db.changelog-version.xml b/docs/db/changelog/changesets/powerauth-push-server/1.7.x/db.changelog-version.xml new file mode 100644 index 000000000..6e97df5f7 --- /dev/null +++ b/docs/db/changelog/changesets/powerauth-push-server/1.7.x/db.changelog-version.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/docs/db/changelog/changesets/powerauth-push-server/db.changelog-module.xml b/docs/db/changelog/changesets/powerauth-push-server/db.changelog-module.xml index 93573e557..0253cdde1 100644 --- a/docs/db/changelog/changesets/powerauth-push-server/db.changelog-module.xml +++ b/docs/db/changelog/changesets/powerauth-push-server/db.changelog-module.xml @@ -9,5 +9,6 @@ + diff --git a/pom.xml b/pom.xml index 7ccc5d960..02c6850a8 100644 --- a/pom.xml +++ b/pom.xml @@ -141,6 +141,14 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + external-service + + + org.apache.maven.plugins maven-source-plugin diff --git a/powerauth-push-client/src/main/java/io/getlime/push/client/PushServerClient.java b/powerauth-push-client/src/main/java/io/getlime/push/client/PushServerClient.java index 34bac28a1..7e49c63d0 100644 --- a/powerauth-push-client/src/main/java/io/getlime/push/client/PushServerClient.java +++ b/powerauth-push-client/src/main/java/io/getlime/push/client/PushServerClient.java @@ -499,17 +499,14 @@ public ObjectResponse getUnconfiguredApplicationList /** * Get detail for an application credentials entity. - * @param appId Application credentials entity ID. - * @param includeIos Whether to include iOS details. - * @param includeAndroid Whether to include Android details. + * @param request Application detail request. * @return Application credentials entity detail. * @throws PushServerClientException Thrown when communication with Push Server fails. */ - public ObjectResponse getApplicationDetail(String appId, boolean includeIos, boolean includeAndroid) throws PushServerClientException { - GetApplicationDetailRequest request = new GetApplicationDetailRequest(appId, includeIos, includeAndroid); - logger.info("Calling push server to retrieve application detail, ID: {} - start", appId); + public ObjectResponse getApplicationDetail(final GetApplicationDetailRequest request) throws PushServerClientException { + logger.info("Calling push server to retrieve application detail, ID: {} - start", request.getAppId()); final ObjectResponse response = postObjectImpl("/admin/app/detail", new ObjectRequest<>(request), GetApplicationDetailResponse.class); - logger.info("Calling push server to retrieve application detail, ID: {} - finish", appId); + logger.info("Calling push server to retrieve application detail, ID: {} - finish", request.getAppId()); return response; } @@ -592,6 +589,35 @@ public Response removeAndroid(String appId) throws PushServerClientException { return response; } + /** + * Update Huawei details for an application credentials entity. + * + * @param request Update Huawei request. + * @return Response from server. + * @throws PushServerClientException Thrown when communication with Push Server fails. + */ + public Response updateHuawei(final UpdateHuaweiRequest request) throws PushServerClientException { + logger.info("Calling push server to update Huawei, ID: {} - start", request.getAppId()); + final Response response = putObjectImpl("/admin/app/huawei/update", new ObjectRequest<>(request)); + logger.info("Calling push server to update Huawei, ID: {} - finish", request.getAppId()); + return response; + } + + /** + * Remove Huawei record from an application credentials entity. + * + * @param appId Application credentials entity ID. + * @return Response from server. + * @throws PushServerClientException Thrown when communication with Push Server fails. + */ + public Response removeHuawei(String appId) throws PushServerClientException { + final RemoveHuaweiRequest request = new RemoveHuaweiRequest(appId); + logger.info("Calling push server to remove Huawei, ID: {} - start", appId); + final Response response = postObjectImpl("/admin/app/huawei/remove", new ObjectRequest<>(request)); + logger.info("Calling push server to remove Huawei, ID: {} - finish", appId); + return response; + } + /** * Post a message to an inbox of provided user. * @param request Request with the message detail. diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/entity/PushMessageSendResult.java b/powerauth-push-model/src/main/java/io/getlime/push/model/entity/PushMessageSendResult.java index aadee3b79..8855fab4b 100644 --- a/powerauth-push-model/src/main/java/io/getlime/push/model/entity/PushMessageSendResult.java +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/entity/PushMessageSendResult.java @@ -39,12 +39,18 @@ public class PushMessageSendResult extends BasePushMessageSendResult { */ private final PlatformResult android; + /** + * Data associated with push messages sent to Huawei devices. + */ + private final PlatformResult huawei; + /** * Default constructor. */ public PushMessageSendResult() { this.ios = new PlatformResult(); this.android = new PlatformResult(); + this.huawei = new PlatformResult(); } /** @@ -56,6 +62,7 @@ public PushMessageSendResult(Mode mode) { super(mode); this.ios = new PlatformResult(); this.android = new PlatformResult(); + this.huawei = new PlatformResult(); } /** diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/entity/PushServerApplication.java b/powerauth-push-model/src/main/java/io/getlime/push/model/entity/PushServerApplication.java index 6e41614cf..3595807e0 100644 --- a/powerauth-push-model/src/main/java/io/getlime/push/model/entity/PushServerApplication.java +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/entity/PushServerApplication.java @@ -16,62 +16,36 @@ package io.getlime.push.model.entity; +import lombok.Getter; +import lombok.Setter; + /** * Push server application credentials entity. * * @author Roman Strobl, roman.strobl@wultra.com */ +@Getter +@Setter public class PushServerApplication { - private String appId; - private Boolean ios; - private Boolean android; - /** - * Get application ID. - * @return Application ID. + * Application ID. */ - public String getAppId() { - return appId; - } - - /** - * Set application ID. - * @param appId Application ID. - */ - public void setAppId(String appId) { - this.appId = appId; - } + private String appId; /** - * Get whether iOS is configured. - * @return Whether iOS is configured. + * Whether iOS is configured. */ - public Boolean getIos() { - return ios; - } + private Boolean ios; /** - * Set whether iOS is configured. - * @param ios Whether iOS is configured. + * Whether Android is configured. */ - public void setIos(Boolean ios) { - this.ios = ios; - } + private Boolean android; /** - * Get whether Android is configured. - * @return Whether Android is configured. + * Whether Huawei is configured. */ - public Boolean getAndroid() { - return android; - } + private Boolean huawei; - /** - * Set whether Android is configured. - * @param android Whether Android is configured. - */ - public void setAndroid(Boolean android) { - this.android = android; - } } diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/enumeration/MobilePlatform.java b/powerauth-push-model/src/main/java/io/getlime/push/model/enumeration/MobilePlatform.java index 9891520a7..0d37eafcd 100644 --- a/powerauth-push-model/src/main/java/io/getlime/push/model/enumeration/MobilePlatform.java +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/enumeration/MobilePlatform.java @@ -35,6 +35,12 @@ public enum MobilePlatform { * Android Platform. */ @JsonProperty("android") - ANDROID + ANDROID, + + /** + * Huawei Platform. + */ + @JsonProperty("huawei") + HUAWEI } diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/request/CreateDeviceForActivationsRequest.java b/powerauth-push-model/src/main/java/io/getlime/push/model/request/CreateDeviceForActivationsRequest.java index 8eb96b045..b092d5519 100644 --- a/powerauth-push-model/src/main/java/io/getlime/push/model/request/CreateDeviceForActivationsRequest.java +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/request/CreateDeviceForActivationsRequest.java @@ -44,10 +44,10 @@ public class CreateDeviceForActivationsRequest { private String appId; /** - * The push token is the value received from APNS, or FCM services without any modification. + * The push token is the value received from APNS, FCM, or HMS services without any modification. */ @NotBlank - @Schema(description = "The push token is the value received from APNS, or FCM services without any modification.") + @Schema(description = "The push token is the value received from APNS, FCM, or HMS services without any modification.") private String token; /** diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/request/CreateDeviceRequest.java b/powerauth-push-model/src/main/java/io/getlime/push/model/request/CreateDeviceRequest.java index 30ab39f81..7c5192f84 100644 --- a/powerauth-push-model/src/main/java/io/getlime/push/model/request/CreateDeviceRequest.java +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/request/CreateDeviceRequest.java @@ -39,10 +39,10 @@ public class CreateDeviceRequest { private String appId; /** - * The push token is the value received from APNS, or FCM services without any modification. + * The push token is the value received from APNS, FCM, or HMS services without any modification. */ @NotBlank - @Schema(description = "The push token is the value received from APNS, or FCM services without any modification.") + @Schema(description = "The push token is the value received from APNS, FCM, or HMS services without any modification.") private String token; @NotNull diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/request/GetApplicationDetailRequest.java b/powerauth-push-model/src/main/java/io/getlime/push/model/request/GetApplicationDetailRequest.java index e8878f409..ecffa6c84 100644 --- a/powerauth-push-model/src/main/java/io/getlime/push/model/request/GetApplicationDetailRequest.java +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/request/GetApplicationDetailRequest.java @@ -17,16 +17,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; /** * Get application credentials entity detail request. * * @author Roman Strobl, roman.strobl@wultra.com */ -@Getter -@Setter +@Data public class GetApplicationDetailRequest { /** @@ -48,6 +46,12 @@ public class GetApplicationDetailRequest { @Schema(description = "Whether to include Android details.") private boolean includeAndroid; + /** + * Whether to include Huawei details. + */ + @Schema(description = "Whether to include Huawei details.") + private boolean includeHuawei; + /** * Default constructor. */ @@ -68,10 +72,11 @@ public GetApplicationDetailRequest(String appId) { * @param includeIos Whether to include iOS details. * @param includeAndroid Whether to include Android details. */ - public GetApplicationDetailRequest(String appId, boolean includeIos, boolean includeAndroid) { + public GetApplicationDetailRequest(String appId, boolean includeIos, boolean includeAndroid, boolean includeHuawei) { this.appId = appId; this.includeIos = includeIos; this.includeAndroid = includeAndroid; + this.includeHuawei = includeHuawei; } } diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/request/RemoveHuaweiRequest.java b/powerauth-push-model/src/main/java/io/getlime/push/model/request/RemoveHuaweiRequest.java new file mode 100644 index 000000000..f453c2f44 --- /dev/null +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/request/RemoveHuaweiRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.push.model.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** + * Remove Huawei configuration request. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Getter +@Setter +public class RemoveHuaweiRequest { + + /** + * Application ID. + */ + @NotBlank + @Schema(description = "Application ID.") + private String appId; + + /** + * No-arg constructor. + */ + public RemoveHuaweiRequest() { + } + + /** + * Constructor with application credentials entity ID. + * @param appId Application credentials entity ID. + */ + public RemoveHuaweiRequest(String appId) { + this.appId = appId; + } + +} diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/request/UpdateHuaweiRequest.java b/powerauth-push-model/src/main/java/io/getlime/push/model/request/UpdateHuaweiRequest.java new file mode 100644 index 000000000..1373efd53 --- /dev/null +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/request/UpdateHuaweiRequest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.push.model.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** + * Update Huawei configuration request. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Getter +@Setter +public class UpdateHuaweiRequest { + + /** + * Application ID. + */ + @NotBlank + @Schema(description = "Application ID.") + private String appId; + + /** + * Huawei project ID. + */ + @NotBlank + @Schema(description = "Huawei project ID.") + private String projectId; + + /** + * Huawei OAuth 2.0 client ID. + */ + @NotBlank + @Schema(description = "Huawei OAuth 2.0 client ID.") + private String clientId; + + /** + * Huawei OAuth 2.0 client secret. + */ + @NotBlank + @Schema(description = "Huawei OAuth 2.0 client secret.") + private String clientSecret; + +} diff --git a/powerauth-push-model/src/main/java/io/getlime/push/model/response/GetApplicationDetailResponse.java b/powerauth-push-model/src/main/java/io/getlime/push/model/response/GetApplicationDetailResponse.java index dfa7894b3..ed448226c 100644 --- a/powerauth-push-model/src/main/java/io/getlime/push/model/response/GetApplicationDetailResponse.java +++ b/powerauth-push-model/src/main/java/io/getlime/push/model/response/GetApplicationDetailResponse.java @@ -16,16 +16,14 @@ package io.getlime.push.model.response; import io.getlime.push.model.entity.PushServerApplication; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; /** * Get application credentials entity detail response. * * @author Roman Strobl, roman.strobl@wultra.com */ -@Getter -@Setter +@Data public class GetApplicationDetailResponse { /** @@ -58,4 +56,9 @@ public class GetApplicationDetailResponse { */ private String androidProjectId; + /** + * Huawei project ID. + */ + private String huaweiProjectId; + } diff --git a/powerauth-push-server/pom.xml b/powerauth-push-server/pom.xml index 2f0b946ce..c043e009c 100644 --- a/powerauth-push-server/pom.xml +++ b/powerauth-push-server/pom.xml @@ -93,6 +93,10 @@ + + org.springframework.boot + spring-boot-starter-oauth2-client + diff --git a/powerauth-push-server/src/main/java/io/getlime/push/configuration/PushServiceConfiguration.java b/powerauth-push-server/src/main/java/io/getlime/push/configuration/PushServiceConfiguration.java index bed523258..00902c7b4 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/configuration/PushServiceConfiguration.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/configuration/PushServiceConfiguration.java @@ -137,6 +137,55 @@ public class PushServiceConfiguration { @Value("${powerauth.push.service.fcm.sendMessageUrl}") private String fcmSendMessageUrl; + /** + * Flag indicating if proxy is enabled for HMS communication. + */ + @Value("${powerauth.push.service.hms.proxy.enabled}") + private boolean hmsProxyEnabled; + + /** + * HMS proxy URL. + */ + @Value("${powerauth.push.service.hms.proxy.host}") + private String hmsProxyHost; + + /** + * HMS proxy port. + */ + @Value("${powerauth.push.service.hms.proxy.port}") + private int hmsProxyPort; + + /** + * HMS proxy username. + */ + @Value("${powerauth.push.service.hms.proxy.username}") + private String hmsProxyUsername; + + /** + * HMS proxy password. + */ + @Value("${powerauth.push.service.hms.proxy.password}") + private String hmsProxyPassword; + + /** + * Get status if notification is set to be sent only through data map + * True in case HMS notification should always be a "data" notification, even for messages with title and message, false otherwise. + */ + @Value("${powerauth.push.service.hms.dataNotificationOnly}") + private boolean hmsDataNotificationOnly; + + /** + * HMS send message endpoint URL. + */ + @Value("${powerauth.push.service.hms.sendMessageUrl}") + private String hmsSendMessageUrl; + + /** + * HMS OAuth service URL to obtain an access token. + */ + @Value("${powerauth.push.service.hms.tokenUrl}") + private String hmsTokenUrl; + /** * The batch size used while sending a push campaign. */ @@ -167,6 +216,24 @@ public class PushServiceConfiguration { @Value("${powerauth.push.service.apns.connect.timeout}") private int apnsConnectTimeout; + /** + * HMS connect timeout. + */ + @Value("${powerauth.push.service.hms.connect.timeout}") + private Duration hmsConnectTimeout; + + /** + * HMS maximum duration allowed between each network-level read operations. + */ + @Value("${powerauth.push.service.hms.response.timeout}") + private Duration hmsResponseTimeout; + + /** + * HMS ConnectionProvider max idle time. + */ + @Value("${powerauth.push.service.hms.max-idle-time}") + private Duration hmsMaxIdleTime; + /** * APNS concurrent connections. */ diff --git a/powerauth-push-server/src/main/java/io/getlime/push/controller/rest/AdministrationController.java b/powerauth-push-server/src/main/java/io/getlime/push/controller/rest/AdministrationController.java index 6ccac62d4..8b379ef14 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/controller/rest/AdministrationController.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/controller/rest/AdministrationController.java @@ -32,6 +32,7 @@ import io.getlime.push.repository.model.AppCredentialsEntity; import io.getlime.push.service.batch.storage.AppCredentialStorageMap; import io.getlime.push.service.http.HttpCustomizationService; +import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -85,6 +86,7 @@ public ObjectResponse listApplications() { app.setAppId(appCredentialsEntity.getAppId()); app.setIos(appCredentialsEntity.getIosPrivateKey() != null); app.setAndroid(appCredentialsEntity.getAndroidPrivateKey() != null); + app.setHuawei(isHuawei(appCredentialsEntity)); appList.add(app); } response.setApplicationList(appList); @@ -150,6 +152,7 @@ public ObjectResponse getApplicationDetail(@Reques app.setAppId(appCredentialsEntity.getAppId()); app.setIos(appCredentialsEntity.getIosPrivateKey() != null); app.setAndroid(appCredentialsEntity.getAndroidPrivateKey() != null); + app.setHuawei(isHuawei(appCredentialsEntity)); response.setApplication(app); if (requestObject.isIncludeIos()) { response.setIosBundle(appCredentialsEntity.getIosBundle()); @@ -160,10 +163,17 @@ public ObjectResponse getApplicationDetail(@Reques if (requestObject.isIncludeAndroid()) { response.setAndroidProjectId(appCredentialsEntity.getAndroidProjectId()); } + if (requestObject.isIncludeHuawei()) { + response.setHuaweiProjectId(appCredentialsEntity.getHmsProjectId()); + } logger.debug("The getApplicationDetail request succeeded"); return new ObjectResponse<>(response); } + private static boolean isHuawei(final AppCredentialsEntity appCredentialsEntity) { + return appCredentialsEntity.getHmsClientSecret() != null && appCredentialsEntity.getHmsClientId() != null; + } + /** * Create application. * @param request Create application request. @@ -205,7 +215,7 @@ public Response updateIos(@RequestBody ObjectRequest request) if (requestObject == null) { throw new PushServerException("Request object must not be empty"); } - logger.info("Received updateIos request, application credentials entity ID: {}", requestObject.getAppId()); + logger.info("Received updateIos request, application ID: {}", requestObject.getAppId()); final String errorMessage = UpdateIosRequestValidator.validate(requestObject); if (errorMessage != null) { throw new PushServerException(errorMessage); @@ -219,7 +229,7 @@ public Response updateIos(@RequestBody ObjectRequest request) appCredentialsEntity.setIosEnvironment(requestObject.getEnvironment()); appCredentialsRepository.save(appCredentialsEntity); appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The updateIos request succeeded, application credentials entity ID: {}", requestObject.getAppId()); + logger.info("The updateIos request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); return new Response(); } @@ -235,7 +245,7 @@ public Response removeIos(@RequestBody ObjectRequest request) if (requestObject == null) { throw new PushServerException("Request object must not be empty"); } - logger.info("Received removeIos request, application credentials entity ID: {}", requestObject.getAppId()); + logger.info("Received removeIos request, application ID: {}", requestObject.getAppId()); String errorMessage = RemoveIosRequestValidator.validate(requestObject); if (errorMessage != null) { throw new PushServerException(errorMessage); @@ -248,7 +258,7 @@ public Response removeIos(@RequestBody ObjectRequest request) appCredentialsEntity.setIosEnvironment(null); appCredentialsRepository.save(appCredentialsEntity); appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The removeIos request succeeded, application credentials entity ID: {}", requestObject.getAppId()); + logger.info("The removeIos request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); return new Response(); } @@ -264,7 +274,7 @@ public Response updateAndroid(@RequestBody ObjectRequest r if (requestObject == null) { throw new PushServerException("Request object must not be empty"); } - logger.info("Received updateAndroid request, application credentials entity ID: {}", requestObject.getAppId()); + logger.info("Received updateAndroid request, application ID: {}", requestObject.getAppId()); String errorMessage = UpdateAndroidRequestValidator.validate(requestObject); if (errorMessage != null) { throw new PushServerException(errorMessage); @@ -275,7 +285,7 @@ public Response updateAndroid(@RequestBody ObjectRequest r appCredentialsEntity.setAndroidProjectId(requestObject.getProjectId()); appCredentialsRepository.save(appCredentialsEntity); appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The updateAndroid request succeeded, application credentials entity ID: {}", requestObject.getAppId()); + logger.info("The updateAndroid request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); return new Response(); } @@ -291,7 +301,7 @@ public Response removeAndroid(@RequestBody ObjectRequest r if (requestObject == null) { throw new PushServerException("Request object must not be empty"); } - logger.info("Received removeAndroid request, application credentials entity ID: {}", requestObject.getAppId()); + logger.info("Received removeAndroid request, application ID: {}", requestObject.getAppId()); String errorMessage = RemoveAndroidRequestValidator.validate(requestObject); if (errorMessage != null) { throw new PushServerException(errorMessage); @@ -301,7 +311,51 @@ public Response removeAndroid(@RequestBody ObjectRequest r appCredentialsEntity.setAndroidProjectId(null); appCredentialsRepository.save(appCredentialsEntity); appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The removeAndroid request succeeded, application credentials entity ID: {}", requestObject.getAppId()); + logger.info("The removeAndroid request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + return new Response(); + } + + /** + * Update Huawei configuration. + * + * @param request Update Huawei configuration request. + * @return Response. + * @throws PushServerException Thrown when application credentials entity could not be found or request validation fails. + */ + @RequestMapping(value = "huawei/update", method = { RequestMethod.POST, RequestMethod.PUT }) + public Response updateHuawei(@Valid @RequestBody ObjectRequest request) throws PushServerException { + final UpdateHuaweiRequest requestObject = request.getRequestObject(); + logger.info("Received update Huawei request, application ID: {}", requestObject.getAppId()); + + final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); + appCredentialsEntity.setHmsProjectId(requestObject.getProjectId()); + appCredentialsEntity.setHmsClientId(requestObject.getClientId()); + appCredentialsEntity.setHmsClientSecret(requestObject.getClientSecret()); + appCredentialsRepository.save(appCredentialsEntity); + appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); + logger.info("The update Huawei request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + return new Response(); + } + + /** + * Remove Huawei configuration. + * + * @param request Remove Huawei configuration request. + * @return Response. + * @throws PushServerException Thrown when application credentials entity could not be found or request validation fails. + */ + @RequestMapping(value = "huawei/remove", method = { RequestMethod.POST, RequestMethod.DELETE }) + public Response removeHuawei(@Valid @RequestBody ObjectRequest request) throws PushServerException { + final RemoveHuaweiRequest requestObject = request.getRequestObject(); + logger.info("Received remove Huawei request, application ID: {}", requestObject.getAppId()); + + final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); + appCredentialsEntity.setHmsProjectId(null); + appCredentialsEntity.setHmsClientSecret(null); + appCredentialsEntity.setHmsClientId(null); + appCredentialsRepository.save(appCredentialsEntity); + appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); + logger.info("The remove Huawei request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); return new Response(); } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/repository/model/AppCredentialsEntity.java b/powerauth-push-server/src/main/java/io/getlime/push/repository/model/AppCredentialsEntity.java index 173b78bf1..c51cda195 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/repository/model/AppCredentialsEntity.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/repository/model/AppCredentialsEntity.java @@ -24,7 +24,7 @@ import java.io.Serializable; /** - * Class representing application tokens used to authenticate against APNs, or FCM services. + * Class representing application tokens used to authenticate against APNs, FCM, or HMS services. * * @author Petr Dvorak, petr@wultra.com */ @@ -94,4 +94,22 @@ public class AppCredentialsEntity implements Serializable { @Column(name = "android_project_id") private String androidProjectId; + /** + * Project ID defined in Huawei AppGallery Connect. + */ + @Column(name = "hms_project_id") + private String hmsProjectId; + + /** + * Huawei OAuth 2.0 Client ID. + */ + @Column(name = "hms_client_id") + private String hmsClientId; + + /** + * Huawei OAuth 2.0 Client Secret. + */ + @Column(name = "hms_client_secret") + private String hmsClientSecret; + } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/repository/model/Platform.java b/powerauth-push-server/src/main/java/io/getlime/push/repository/model/Platform.java index 8a8cd3778..df936ecf4 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/repository/model/Platform.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/repository/model/Platform.java @@ -30,5 +30,10 @@ public enum Platform { /** * Android Platform. */ - ANDROID + ANDROID, + + /** + * Huawei Platform. + */ + HUAWEI; } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/repository/model/PlatformConverter.java b/powerauth-push-server/src/main/java/io/getlime/push/repository/model/PlatformConverter.java index f4c8d31b6..1300c41ad 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/repository/model/PlatformConverter.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/repository/model/PlatformConverter.java @@ -36,6 +36,7 @@ public String convertToDatabaseColumn(final Platform attribute) { return switch (attribute) { case IOS -> "ios"; case ANDROID -> "android"; + case HUAWEI -> "huawei"; }; } @@ -48,6 +49,7 @@ public Platform convertToEntityAttribute(final String dbData) { return switch (dbData) { case "ios" -> Platform.IOS; case "android" -> Platform.ANDROID; + case "huawei" -> Platform.HUAWEI; default -> throw new IllegalArgumentException("No mapping for platform: " + dbData); }; } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/AppRelatedPushClient.java b/powerauth-push-server/src/main/java/io/getlime/push/service/AppRelatedPushClient.java index 0552a6765..8ae9a8605 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/AppRelatedPushClient.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/AppRelatedPushClient.java @@ -18,12 +18,17 @@ import com.eatthepath.pushy.apns.ApnsClient; import io.getlime.push.repository.model.AppCredentialsEntity; import io.getlime.push.service.fcm.FcmClient; +import io.getlime.push.service.hms.HmsClient; +import lombok.Getter; +import lombok.Setter; /** * Class storing app credentials and clients. * * @author Petr Dvorak, petr@wultra.com */ +@Getter +@Setter public class AppRelatedPushClient { /** @@ -42,50 +47,8 @@ public class AppRelatedPushClient { private FcmClient fcmClient; /** - * Get app credentials. - * @return App credentials. + * HMS client instance, used for Huawei Mobile Services. */ - public AppCredentialsEntity getAppCredentials() { - return appCredentials; - } + private HmsClient hmsClient; - /** - * Set app credentials. - * @param appCredentials App credentials. - */ - public void setAppCredentials(AppCredentialsEntity appCredentials) { - this.appCredentials = appCredentials; - } - - /** - * Get APNs client. - * @return APNs client. - */ - public ApnsClient getApnsClient() { - return apnsClient; - } - - /** - * Set APNs client. - * @param apnsClient APNs client. - */ - public void setApnsClient(ApnsClient apnsClient) { - this.apnsClient = apnsClient; - } - - /** - * Get FCM client. - * @return FCM client. - */ - public FcmClient getFcmClient() { - return fcmClient; - } - - /** - * Set FCM client. - * @param fcmClient FCM client. - */ - public void setFcmClient(FcmClient fcmClient) { - this.fcmClient = fcmClient; - } } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/DeviceRegistrationService.java b/powerauth-push-server/src/main/java/io/getlime/push/service/DeviceRegistrationService.java index 2998ebf4e..df8bc9963 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/DeviceRegistrationService.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/DeviceRegistrationService.java @@ -261,6 +261,7 @@ private static Platform convert(final MobilePlatform source) { return switch (source) { case IOS -> Platform.IOS; case ANDROID -> Platform.ANDROID; + case HUAWEI -> Platform.HUAWEI; }; } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/PushMessageSenderService.java b/powerauth-push-server/src/main/java/io/getlime/push/service/PushMessageSenderService.java index a7dae9ba6..d662b1500 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/PushMessageSenderService.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/PushMessageSenderService.java @@ -32,6 +32,7 @@ import io.getlime.push.repository.model.PushMessageEntity; import io.getlime.push.service.batch.storage.AppCredentialStorageMap; import io.getlime.push.service.fcm.FcmClient; +import io.getlime.push.service.hms.HmsClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -141,6 +142,15 @@ public BasePushMessageSendResult sendPushMessage(final String appId, final Mode final String token = device.getPushToken(); final PushMessageSendResult.PlatformResult platformResult = sendResult.getAndroid(); pushSendingWorker.sendMessageToAndroid(pushClient.getFcmClient(), pushMessage.getBody(), pushMessage.getAttributes(), pushMessage.getPriority(), token, createPushSendingCallback(mode, device, platformResult, pushMessageObject, phaser)); + } else if (platform == Platform.HUAWEI) { + if (pushClient.getHmsClient() == null) { + logger.error("Push message cannot be sent to HMS because HMS is not configured in push server."); + arriveAndDeregisterPhaserForMode(phaser, mode); + continue; + } + final String token = device.getPushToken(); + final PushMessageSendResult.PlatformResult platformResult = sendResult.getHuawei(); + pushSendingWorker.sendMessageToHuawei(pushClient.getHmsClient(), pushMessage.getBody(), pushMessage.getAttributes(), pushMessage.getPriority(), token, createPushSendingCallback(mode, device, platformResult, pushMessageObject, phaser)); } } } @@ -225,6 +235,8 @@ public void sendCampaignMessage(final String appId, Platform platform, final Str pushSendingWorker.sendMessageToIos(pushClient.getApnsClient(), pushMessageBody, attributes, priority, token, pushClient.getAppCredentials().getIosBundle(), createPushSendingCallback(token, pushMessageObject, pushClient)); case ANDROID -> pushSendingWorker.sendMessageToAndroid(pushClient.getFcmClient(), pushMessageBody, attributes, priority, token, createPushSendingCallback(token, pushMessageObject, pushClient)); + case HUAWEI -> + pushSendingWorker.sendMessageToHuawei(pushClient.getHmsClient(), pushMessageBody, attributes, priority, token, createPushSendingCallback(token, pushMessageObject, pushClient)); } } @@ -261,7 +273,7 @@ private List getPushDevices(Long id, String userId } } - // Prepare and cache APNS and FCM clients for provided app + // Prepare and cache APNS, FCM, and HMS clients for provided app private AppRelatedPushClient prepareClients(String appId) throws PushServerException { synchronized (this) { AppRelatedPushClient pushClient = appRelatedPushClientMap.get(appId); @@ -276,9 +288,13 @@ private AppRelatedPushClient prepareClients(String appId) throws PushServerExcep final FcmClient fcmClient = pushSendingWorker.prepareFcmClient(credentials.getAndroidProjectId(), credentials.getAndroidPrivateKey()); pushClient.setFcmClient(fcmClient); } + if (credentials.getHmsClientId() != null) { + final HmsClient hmsClient = pushSendingWorker.prepareHmsClient(credentials); + pushClient.setHmsClient(hmsClient); + } pushClient.setAppCredentials(credentials); appRelatedPushClientMap.put(appId, pushClient); - logger.info("Creating APNS and FCM clients for app {}", appId); + logger.info("Creating APNS, FCM, and HMS clients for app {}", appId); } return pushClient; } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/PushSendingWorker.java b/powerauth-push-server/src/main/java/io/getlime/push/service/PushSendingWorker.java index b352c315c..d53fb7d0b 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/PushSendingWorker.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/PushSendingWorker.java @@ -22,6 +22,8 @@ import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification; import com.eatthepath.pushy.apns.util.TokenUtil; import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.firebase.messaging.AndroidConfig; import com.google.firebase.messaging.AndroidNotification; import com.google.firebase.messaging.Message; @@ -33,10 +35,14 @@ import io.getlime.push.model.entity.PushMessageAttributes; import io.getlime.push.model.entity.PushMessageBody; import io.getlime.push.model.enumeration.Priority; +import io.getlime.push.repository.model.AppCredentialsEntity; import io.getlime.push.service.apns.ApnsRejectionReason; import io.getlime.push.service.fcm.FcmClient; import io.getlime.push.service.fcm.FcmModelConverter; import io.getlime.push.service.fcm.model.FcmSuccessResponse; +import io.getlime.push.service.hms.HmsClient; +import io.getlime.push.service.hms.HmsSendResponse; +import io.getlime.push.service.hms.request.AndroidNotification.Importance; import io.getlime.push.util.CaCertUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,6 +137,17 @@ FcmClient prepareFcmClient(String projectId, byte[] privateKey) throws PushServe return fcmClient; } + /** + * Prepares an HMS (Huawei Mobile Services) service client. + * + * @param credentials Credentials. + * @return A new instance of HMS client. + */ + HmsClient prepareHmsClient(final AppCredentialsEntity credentials) { + logger.info("Initializing HmsClient"); + return new HmsClient(pushServiceConfiguration, credentials); + } + /** * Send message to Android platform. * @param fcmClient Instance of the FCM client used for sending the notifications. @@ -210,6 +227,40 @@ void sendMessageToAndroid(final FcmClient fcmClient, final PushMessageBody pushM } } + /** + * Send message to Huawei platform. + * + * @param hmsClient Instance of the HMS client used for sending the notifications. + * @param pushMessageBody Push message contents. + * @param attributes Push message attributes. + * @param priority Push message priority. + * @param pushToken Push token used to deliver the message. + * @param callback Callback that is called after the asynchronous executions is completed. + * @throws PushServerException In case any issue happens while sending the push message. + */ + void sendMessageToHuawei(final HmsClient hmsClient, final PushMessageBody pushMessageBody, final PushMessageAttributes attributes, final Priority priority, final String pushToken, final PushSendingCallback callback) throws PushServerException { + final io.getlime.push.service.hms.request.Message message = buildHmsMessage(pushMessageBody, attributes, priority, pushToken); + + final Consumer successConsumer = response -> { + final String requestId = response.requestId(); + if (HmsClient.SUCCESS_CODE.equals(response.code())) { + logger.info("Notification sent successfully, request ID: {}", requestId); + callback.didFinishSendingMessage(PushSendingCallback.Result.OK); + } else { + logger.error("Notification sending failed, request ID: {}, code: {}, message: {}", requestId, response.code(), response.msg()); + callback.didFinishSendingMessage(PushSendingCallback.Result.FAILED); + } + }; + + final Consumer throwableConsumer = throwable -> { + logger.error("Invalid response received from HSM, notification sending failed.", throwable); + callback.didFinishSendingMessage(PushSendingCallback.Result.FAILED); + }; + + hmsClient.sendMessage(message, false) + .subscribe(successConsumer, throwableConsumer); + } + /** * Build Android Message object from Push message body. * @param pushMessageBody Push message body. @@ -269,6 +320,66 @@ private Message buildAndroidMessage(final PushMessageBody pushMessageBody, final .build(); } + /** + * Build HMS Message object from Push message body. + * + * @param pushMessageBody Push message body. + * @param attributes Push message attributes. + * @param priority Push message priority. + * @param pushToken Push token. + * @return HMS Message object. + * @throws PushServerException In case any issue happens while building the push message. + */ + private io.getlime.push.service.hms.request.Message buildHmsMessage(final PushMessageBody pushMessageBody, final PushMessageAttributes attributes, final Priority priority, final String pushToken) throws PushServerException { + final var androidConfigBuilder = io.getlime.push.service.hms.request.AndroidConfig.builder() + .collapseKey(Integer.valueOf(pushMessageBody.getCollapseKey())); + + calculateTtl(pushMessageBody.getValidUntil()) + .map(Object::toString) + .ifPresent(androidConfigBuilder::ttl); + + final Importance importance = (priority == Priority.NORMAL) ? Importance.NORMAL : Importance.HIGH; + + final var notificationBuilder = io.getlime.push.service.hms.request.AndroidNotification.builder() + .importance(importance) + .title(pushMessageBody.getTitle()) + .titleLocKey(pushMessageBody.getTitleLocKey()) + .body(pushMessageBody.getBody()) + .bodyLocKey(pushMessageBody.getBodyLocKey()) + .icon(pushMessageBody.getIcon()) + .sound(pushMessageBody.getSound()) + .tag(pushMessageBody.getCategory()); + + if (pushMessageBody.getTitleLocArgs() != null) { + notificationBuilder.titleLocArgs(List.of(pushMessageBody.getTitleLocArgs())); + } + if (pushMessageBody.getBodyLocArgs() != null) { + notificationBuilder.bodyLocArgs(List.of(pushMessageBody.getBodyLocArgs())); + } + + if (isMessageNotSilent(attributes)) { + androidConfigBuilder.notification(notificationBuilder.build()); + } + + final Map extras = pushMessageBody.getExtras(); + final String data; + if (extras == null) { + data = null; + } else { + try { + data = new ObjectMapper().writeValueAsString(extras); + } catch (JsonProcessingException e) { + throw new PushServerException("Failed to serialize extras to JSON", e); + } + } + + return io.getlime.push.service.hms.request.Message.builder() + .token(List.of(pushToken)) + .android(androidConfigBuilder.build()) + .data(data) + .build(); + } + private static boolean isMessageNotSilent(final PushMessageAttributes attributes) { // if there are no attributes, assume the message is not silent return attributes == null || !attributes.getSilent(); diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/hms/HmsClient.java b/powerauth-push-server/src/main/java/io/getlime/push/service/hms/HmsClient.java new file mode 100644 index 000000000..c6177bbef --- /dev/null +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/hms/HmsClient.java @@ -0,0 +1,156 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.push.service.hms; + +import io.getlime.push.configuration.PushServiceConfiguration; +import io.getlime.push.repository.model.AppCredentialsEntity; +import io.getlime.push.service.hms.request.Message; +import io.netty.channel.ChannelOption; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.transport.ProxyProvider; + +import java.time.Duration; +import java.util.Map; + +/** + * HMS (Huawei Mobile Services) server client. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Slf4j +public class HmsClient { + + private static final String OAUTH_REGISTRATION_ID = "hms"; + private static final String OAUTH_USER_AGENT = "Wultra Push-Server"; + + /** + * Success error code. + * + * @see HMS Documentation + */ + public static final String SUCCESS_CODE = "80000000"; + + final WebClient webClient; + final String messageUrl; + + public HmsClient(final PushServiceConfiguration pushServiceConfiguration, final AppCredentialsEntity credentials) { + webClient = createWebClient(credentials.getHmsClientId(), credentials.getHmsClientSecret(), pushServiceConfiguration); + final String projectId = credentials.getHmsProjectId(); + messageUrl = String.format(pushServiceConfiguration.getHmsSendMessageUrl(), projectId); + } + + public Mono sendMessage(final Message message, final boolean validationOnly) { + final Map body = Map.of("validate_only", validationOnly, "message", message); + return webClient.post() + .uri(messageUrl) + .bodyValue(body) + .retrieve() + .bodyToMono(HmsSendResponse.class); + } + + private static WebClient createWebClient( + final String oAuthClientId, + final String oAuthClientSecret, + final PushServiceConfiguration configuration) { + + final AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = authorizedClientServiceReactiveOAuth2AuthorizedClientManager(oAuthClientId, oAuthClientSecret, configuration); + + final ServerOAuth2AuthorizedClientExchangeFilterFunction oAuth2ExchangeFilterFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oAuth2ExchangeFilterFunction.setDefaultClientRegistrationId(OAUTH_REGISTRATION_ID); + + return createWebClient(oAuth2ExchangeFilterFunction, configuration); + } + + private static AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientServiceReactiveOAuth2AuthorizedClientManager( + final String oAuthClientId, + final String oAuthClientSecret, + final PushServiceConfiguration configuration) { + + final ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(OAUTH_REGISTRATION_ID) + .tokenUri(configuration.getHmsTokenUrl()) + .clientName(OAUTH_USER_AGENT) + .clientId(oAuthClientId) + .clientSecret(oAuthClientSecret) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build(); + + final ReactiveClientRegistrationRepository clientRegistrations = new InMemoryReactiveClientRegistrationRepository(clientRegistration); + final ReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations); + + final ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsReactiveOAuth2AuthorizedClientProvider(); + + final AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + return authorizedClientManager; + } + + private static WebClient createWebClient(final ExchangeFilterFunction filter, PushServiceConfiguration configuration) { + final Duration connectionTimeout = configuration.getHmsConnectTimeout(); + final Duration responseTimeout = configuration.getHmsResponseTimeout(); + final Duration maxIdleTime = configuration.getHmsMaxIdleTime(); + logger.info("Setting connectionTimeout: {}, responseTimeout: {}, maxIdleTime: {}", connectionTimeout, responseTimeout, maxIdleTime); + + final ConnectionProvider connectionProvider = ConnectionProvider.builder("custom") + .maxIdleTime(maxIdleTime) + .build(); + HttpClient httpClient = HttpClient.create(connectionProvider) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.toIntExact(connectionTimeout.toMillis())) + .responseTimeout(responseTimeout); + + if (configuration.isHmsProxyEnabled()) { + logger.debug("Configuring proxy {}:{}", configuration.getHmsProxyHost(), configuration.getHmsProxyPort()); + httpClient = httpClient.proxy(proxySpec -> { + final ProxyProvider.Builder proxyBuilder = proxySpec + .type(ProxyProvider.Proxy.HTTP) + .host(configuration.getHmsProxyHost()) + .port(configuration.getHmsProxyPort()); + if (StringUtils.isNotBlank(configuration.getHmsProxyUsername())) { + proxyBuilder.username(configuration.getHmsProxyUsername()); + proxyBuilder.password(s -> configuration.getHmsProxyPassword()); + } + + proxyBuilder.build(); + }); + } + + final ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); + + return WebClient.builder() + .clientConnector(connector) + .filter(filter) + .build(); + } + +} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/hms/HmsSendResponse.java b/powerauth-push-server/src/main/java/io/getlime/push/service/hms/HmsSendResponse.java new file mode 100644 index 000000000..766fc0546 --- /dev/null +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/hms/HmsSendResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.push.service.hms; + +/** + * HMS send response. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +public record HmsSendResponse(String code, String msg, String requestId) { + +} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/AndroidConfig.java b/powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/AndroidConfig.java new file mode 100644 index 000000000..ffc77cb12 --- /dev/null +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/AndroidConfig.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.push.service.hms.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +/** + * HMS (Huawei Mobile Services) json mapping object. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Getter +@SuperBuilder +@Jacksonized +public class AndroidConfig { + + @JsonProperty("collapse_key") + private final Integer collapseKey; + + private final String urgency; + + private final String category; + + private final String ttl; + + @JsonProperty("bi_tag") + private final String biTag; + + @JsonProperty("fast_app_target") + private final Integer fastAppTargetType; + + private final String data; + + private final AndroidNotification notification; + + private final String receiptId; +} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/AndroidNotification.java b/powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/AndroidNotification.java new file mode 100644 index 000000000..a11496240 --- /dev/null +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/hms/request/AndroidNotification.java @@ -0,0 +1,143 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getlime.push.service.hms.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * HMS (Huawei Mobile Services) json mapping object. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Getter +@SuperBuilder +@Jacksonized +public class AndroidNotification { + + private final String title; + + private final String body; + + private final String icon; + + private final String color; + + private final String sound; + + @JsonProperty("default_sound") + private final boolean defaultSound; + + private final String tag; + + @JsonProperty("click_action") + private final ClickAction clickAction; + + @JsonProperty("body_loc_key") + private final String bodyLocKey; + + @Builder.Default + @JsonProperty("body_loc_args") + private final List bodyLocArgs = new ArrayList<>(); + + @JsonProperty("title_loc_key") + private final String titleLocKey; + + @Builder.Default + @JsonProperty("title_loc_args") + private final List titleLocArgs = new ArrayList<>(); + + @JsonProperty("multi_lang_key") + private final Map multiLangKey; + + @JsonProperty("channel_id") + private final String channelId; + + @JsonProperty("notify_summary") + private final String notifySummary; + + private final String image; + + private final Integer style; + + @JsonProperty("big_title") + private final String bigTitle; + + @JsonProperty("big_body") + private final String bigBody; + + @JsonProperty("auto_clear") + private final Integer autoClear; + + @JsonProperty("notify_id") + private final Integer notifyId; + + private final String group; + + private final BadgeNotification badge; + + private final String ticker; + + @JsonProperty("auto_cancel") + private final boolean autoCancel; + + private final String when; + + @JsonProperty("local_only") + private final Boolean localOnly; + + private final Importance importance; + + @JsonProperty("use_default_vibrate") + private final boolean useDefaultVibrate; + + @JsonProperty("use_default_light") + private final boolean useDefaultLight; + + @Builder.Default + @JsonProperty("vibrate_config") + private final List vibrateConfig = new ArrayList<>(); + + private final String visibility; + + @JsonProperty("light_settings") + private final LightSettings lightSettings; + + @JsonProperty("foreground_show") + private final boolean foregroundShow; + + @JsonProperty("inbox_content") + private final List inboxContent; + + private final List