From 604cdd04b0e76960e9d71da1d8217699078bb2f3 Mon Sep 17 00:00:00 2001 From: MK13 Date: Tue, 21 Jul 2020 22:55:29 +0200 Subject: [PATCH] Initial commit for push service --- pom.xml | 71 ++++++++++ push-service-client-lib/pom.xml | 40 ++++++ .../de/pushservice/client/ResponseReason.java | 33 +++++ .../de/pushservice/client/dto/MessageDto.java | 33 +++++ .../client/dto/NotificationRequestDto.java | 22 ++++ .../client/dto/SubscriptionDto.java | 31 +++++ .../de/pushservice/client/model/Urgency.java | 8 ++ .../resources/javascript/service-worker.js | 28 ++++ .../javascript/setup-push-service-client.js | 121 ++++++++++++++++++ push-service-server/pom.xml | 56 ++++++++ .../server/PushServiceApplication.java | 18 +++ .../controller/PushServiceController.java | 38 ++++++ .../server/decorator/MessageDecorator.java | 36 ++++++ .../decorator/SubscriptionDecorator.java | 48 +++++++ 14 files changed, 583 insertions(+) create mode 100644 pom.xml create mode 100644 push-service-client-lib/pom.xml create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/ResponseReason.java create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/dto/MessageDto.java create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/dto/NotificationRequestDto.java create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/dto/SubscriptionDto.java create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/model/Urgency.java create mode 100644 push-service-client-lib/src/main/resources/javascript/service-worker.js create mode 100644 push-service-client-lib/src/main/resources/javascript/setup-push-service-client.js create mode 100644 push-service-server/pom.xml create mode 100644 push-service-server/src/main/java/de/pushservice/server/PushServiceApplication.java create mode 100644 push-service-server/src/main/java/de/pushservice/server/controller/PushServiceController.java create mode 100644 push-service-server/src/main/java/de/pushservice/server/decorator/MessageDecorator.java create mode 100644 push-service-server/src/main/java/de/pushservice/server/decorator/SubscriptionDecorator.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fe01db2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + push-service-client-lib + push-service-server + + + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + + de.77zzcx7 + push-service-parent + 1-SNAPSHOT + pom + A micro service to send push notifications + push-service-parent + + + UTF-8 + 1.9 + 1.9 + 1.9 + + 000001 + + + + + 77zzcx7-snapshots + http://192.168.10.1:8100/repository/77zzcx7-snapshots/ + + + 77zzcx7-releases + http://192.168.10.1:8100/repository/77zzcx7-releases/ + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + nl.martijndwars + web-push + 5.1.0 + + + org.bouncycastle + bcprov-jdk15on + 1.64 + + + + \ No newline at end of file diff --git a/push-service-client-lib/pom.xml b/push-service-client-lib/pom.xml new file mode 100644 index 0000000..bc037b8 --- /dev/null +++ b/push-service-client-lib/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + push-service-parent + de.77zzcx7 + 1-SNAPSHOT + + + push-service-client-lib + jar + Client libraries for push-service + push-service-client-lib + + + + org.bouncycastle + bcprov-jdk15on + + + org.springframework + spring-web + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + \ No newline at end of file diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/ResponseReason.java b/push-service-client-lib/src/main/java/de/pushservice/client/ResponseReason.java new file mode 100644 index 0000000..d2ea593 --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/ResponseReason.java @@ -0,0 +1,33 @@ +package de.pushservice.client; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public enum ResponseReason { + OK(HttpStatus.OK), + UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR); + + private final HttpStatus httpStatus; + + ResponseReason(HttpStatus httpStatus) { + this.httpStatus = httpStatus; + } + + public ResponseEntity toResponseEntity() { + return new ResponseEntity<>(this.name(), this.httpStatus); + } + + public HttpStatus getHttpStatus() { + return this.httpStatus; + } + + public static ResponseReason fromResponseEntity(ResponseEntity entity) { + for (ResponseReason reason : values()) { + if (reason.name().equals(entity.getBody())) { + return reason; + } + } + + return UNKNOWN_ERROR; + } +} diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/dto/MessageDto.java b/push-service-client-lib/src/main/java/de/pushservice/client/dto/MessageDto.java new file mode 100644 index 0000000..37f8105 --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/dto/MessageDto.java @@ -0,0 +1,33 @@ +package de.pushservice.client.dto; + +import de.pushservice.client.model.Urgency; + +public class MessageDto { + private String payload; + private String topic; + private Urgency urgency; + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Urgency getUrgency() { + return urgency; + } + + public void setUrgency(Urgency urgency) { + this.urgency = urgency; + } +} diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/dto/NotificationRequestDto.java b/push-service-client-lib/src/main/java/de/pushservice/client/dto/NotificationRequestDto.java new file mode 100644 index 0000000..b808b45 --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/dto/NotificationRequestDto.java @@ -0,0 +1,22 @@ +package de.pushservice.client.dto; + +public class NotificationRequestDto { + private SubscriptionDto subscriptionDto; + private MessageDto messageDto; + + public SubscriptionDto getSubscriptionDto() { + return subscriptionDto; + } + + public void setSubscriptionDto(SubscriptionDto subscriptionDto) { + this.subscriptionDto = subscriptionDto; + } + + public MessageDto getMessageDto() { + return messageDto; + } + + public void setMessageDto(MessageDto messageDto) { + this.messageDto = messageDto; + } +} diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/dto/SubscriptionDto.java b/push-service-client-lib/src/main/java/de/pushservice/client/dto/SubscriptionDto.java new file mode 100644 index 0000000..fa006ce --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/dto/SubscriptionDto.java @@ -0,0 +1,31 @@ +package de.pushservice.client.dto; + +public class SubscriptionDto { + private String auth; + private String endpoint; + private String key; + + public void setAuth(String auth) { + this.auth = auth; + } + + public String getAuth() { + return auth; + } + + public void setKey(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getEndpoint() { + return endpoint; + } +} diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/model/Urgency.java b/push-service-client-lib/src/main/java/de/pushservice/client/model/Urgency.java new file mode 100644 index 0000000..3efef04 --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/model/Urgency.java @@ -0,0 +1,8 @@ +package de.pushservice.client.model; + +public enum Urgency { + VERY_LOW, + LOW, + NORMAL, + HIGH +} diff --git a/push-service-client-lib/src/main/resources/javascript/service-worker.js b/push-service-client-lib/src/main/resources/javascript/service-worker.js new file mode 100644 index 0000000..b12f85e --- /dev/null +++ b/push-service-client-lib/src/main/resources/javascript/service-worker.js @@ -0,0 +1,28 @@ +'use strict'; + +self.addEventListener('push', function(event) { + console.log('Received push'); + let notificationTitle = 'Hello'; + const notificationOptions = { + body: 'Thanks for sending this push msg.', + icon: './images/logo-192x192.png', + badge: './images/badge-72x72.png', + tag: 'simple-push-demo-notification', + data: { + url: 'https://developers.google.com/web/fundamentals/getting-started/push-notifications/', + }, + }; + + if (event.data) { + const dataText = event.data.text(); + notificationTitle = 'Received Payload'; + notificationOptions.body = `Push data: '${dataText}'`; + } + + event.waitUntil( + Promise.all([ + self.registration.showNotification( + notificationTitle, notificationOptions), + ]) + ); +}); \ No newline at end of file diff --git a/push-service-client-lib/src/main/resources/javascript/setup-push-service-client.js b/push-service-client-lib/src/main/resources/javascript/setup-push-service-client.js new file mode 100644 index 0000000..5c7268e --- /dev/null +++ b/push-service-client-lib/src/main/resources/javascript/setup-push-service-client.js @@ -0,0 +1,121 @@ +/** + * Step one: run a function on load (or whenever is appropriate for you) + * Function run on load sets up the service worker if it is supported in the + * browser. Requires a serviceworker in a `sw.js`. This file contains what will + * happen when we receive a push notification. + * If you are using webpack, see the section below. + */ +$(function () { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/service-worker.js').then(initialiseState); + } else { + console.warn('Service workers are not supported in this browser.'); + } +}); + +/** + * Step two: The serviceworker is registered (started) in the browser. Now we + * need to check if push messages and notifications are supported in the browser + */ +function initialiseState() { + + // Check if desktop notifications are supported + if (!('showNotification' in ServiceWorkerRegistration.prototype)) { + console.warn('Notifications aren\'t supported.'); + return; + } + + // Check if user has disabled notifications + // If a user has manually disabled notifications in his/her browser for + // your page previously, they will need to MANUALLY go in and turn the + // permission back on. In this statement you could show some UI element + // telling the user how to do so. + if (Notification.permission === 'denied') { + console.warn('The user has blocked notifications.'); + return; + } + + // Check is push API is supported + if (!('PushManager' in window)) { + console.warn('Push messaging isn\'t supported.'); + return; + } + + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { + + // Get the push notification subscription object + serviceWorkerRegistration.pushManager.getSubscription().then(function (subscription) { + + // If this is the user's first visit we need to set up + // a subscription to push notifications + if (!subscription) { + subscribe(); + + return; + } + + // Update the server state with the new subscription + sendSubscriptionToServer(subscription); + }) + .catch(function(err) { + // Handle the error - show a notification in the GUI + console.warn('Error during getSubscription()', err); + }); + }); +} + +/** + * Step three: Create a subscription. Contact the third party push server (for + * example mozilla's push server) and generate a unique subscription for the + * current browser. + */ +function subscribe() { + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { + + // Contact the third party push server. Which one is contacted by + // pushManager is configured internally in the browser, so we don't + // need to worry about browser differences here. + // + // When .subscribe() is invoked, a notification will be shown in the + // user's browser, asking the user to accept push notifications from + // . This is why it is async and requires a catch. + serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true}).then(function (subscription) { + + // Update the server state with the new subscription + return sendSubscriptionToServer(subscription); + }) + .catch(function (e) { + if (Notification.permission === 'denied') { + console.warn('Permission for Notifications was denied'); + } else { + console.error('Unable to subscribe to push.', e); + } + }); + }); +} + +/** + * Step four: Send the generated subscription object to our server. + */ +function sendSubscriptionToServer(subscription) { + + // Get public key and user auth from the subscription object + var key = subscription.getKey ? subscription.getKey('p256dh') : ''; + var auth = subscription.getKey ? subscription.getKey('auth') : ''; + + // This example uses the new fetch API. This is not supported in all + // browsers yet. + return fetch('/profile/subscription', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + endpoint: subscription.endpoint, + // Take byte[] and turn it into a base64 encoded string suitable for + // POSTing to a server over HTTP + key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : '', + auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : '' + }) + }); +} \ No newline at end of file diff --git a/push-service-server/pom.xml b/push-service-server/pom.xml new file mode 100644 index 0000000..f41dd97 --- /dev/null +++ b/push-service-server/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + push-service-parent + de.77zzcx7 + 1-SNAPSHOT + + + push-service-server + ${packaging.type} + The server part of the push-service + push-service-server + + + jar + + + + + nl.martijndwars + web-push + + + org.springframework.boot + spring-boot-starter-web + + + de.77zzcx7 + push-service-client-lib + ${version} + + + + + + build-war + + war + + + ${project.artifactId}##${parallelDeploymentVersion} + + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + + \ No newline at end of file diff --git a/push-service-server/src/main/java/de/pushservice/server/PushServiceApplication.java b/push-service-server/src/main/java/de/pushservice/server/PushServiceApplication.java new file mode 100644 index 0000000..26e4175 --- /dev/null +++ b/push-service-server/src/main/java/de/pushservice/server/PushServiceApplication.java @@ -0,0 +1,18 @@ +package de.pushservice.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class PushServiceApplication extends SpringBootServletInitializer { + public static void main(String[] args) { + SpringApplication.run(PushServiceApplication.class); + } + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(PushServiceApplication.class); + } +} \ No newline at end of file diff --git a/push-service-server/src/main/java/de/pushservice/server/controller/PushServiceController.java b/push-service-server/src/main/java/de/pushservice/server/controller/PushServiceController.java new file mode 100644 index 0000000..e06ba8b --- /dev/null +++ b/push-service-server/src/main/java/de/pushservice/server/controller/PushServiceController.java @@ -0,0 +1,38 @@ +package de.pushservice.server.controller; + +import de.pushservice.client.ResponseReason; +import de.pushservice.client.dto.NotificationRequestDto; +import de.pushservice.server.decorator.MessageDecorator; +import de.pushservice.server.decorator.SubscriptionDecorator; +import nl.martijndwars.webpush.Notification; +import nl.martijndwars.webpush.PushService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class PushServiceController { + private static final Logger LOGGER = LoggerFactory.getLogger(PushServiceController.class); + + @PostMapping + public ResponseEntity notify(NotificationRequestDto notificationRequestDto) { + try { + final SubscriptionDecorator subscription = SubscriptionDecorator + .fromDto(notificationRequestDto.getSubscriptionDto()); + final Notification notification = MessageDecorator.fromDto(notificationRequestDto.getMessageDto()) + .toNotification(subscription); + final PushService extPushService = new PushService(); + + extPushService.send(notification); + + return ResponseReason.OK.toResponseEntity(); + } + catch(Exception e) { + LOGGER.error("Error while sending notification!", e); + + return ResponseReason.UNKNOWN_ERROR.toResponseEntity(); + } + } +} diff --git a/push-service-server/src/main/java/de/pushservice/server/decorator/MessageDecorator.java b/push-service-server/src/main/java/de/pushservice/server/decorator/MessageDecorator.java new file mode 100644 index 0000000..a18561e --- /dev/null +++ b/push-service-server/src/main/java/de/pushservice/server/decorator/MessageDecorator.java @@ -0,0 +1,36 @@ +package de.pushservice.server.decorator; + +import de.pushservice.client.dto.MessageDto; +import nl.martijndwars.webpush.Notification; +import nl.martijndwars.webpush.Urgency; + +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.spec.InvalidKeySpecException; + +public class MessageDecorator { + private MessageDto dto; + + private MessageDecorator(MessageDto dto) { + this.dto = dto; + } + + public static final MessageDecorator fromDto(MessageDto dto) { + return new MessageDecorator(dto); + } + + public Notification toNotification(SubscriptionDecorator subscription) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + return Notification.builder() + .endpoint(subscription.getEndpoint()) + .userPublicKey(subscription.getUserPublicKey()) + .userAuth(subscription.getAuthAsBytes()) + .payload(dto.getPayload()) + .urgency(getExtUrgency()) + .topic(dto.getTopic()) + .build(); + } + + private Urgency getExtUrgency() { + return Urgency.valueOf(dto.getUrgency().name()); + } +} diff --git a/push-service-server/src/main/java/de/pushservice/server/decorator/SubscriptionDecorator.java b/push-service-server/src/main/java/de/pushservice/server/decorator/SubscriptionDecorator.java new file mode 100644 index 0000000..f6890be --- /dev/null +++ b/push-service-server/src/main/java/de/pushservice/server/decorator/SubscriptionDecorator.java @@ -0,0 +1,48 @@ +package de.pushservice.server.decorator; + +import de.pushservice.client.dto.SubscriptionDto; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.math.ec.ECPoint; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; + +public class SubscriptionDecorator { + private SubscriptionDto dto; + + private SubscriptionDecorator(SubscriptionDto dto) { + this.dto = dto; + } + + public static final SubscriptionDecorator fromDto(SubscriptionDto dto) { + return new SubscriptionDecorator(dto); + } + + byte[] getAuthAsBytes() { + return Base64.getDecoder().decode(dto.getAuth()); + } + + private byte[] getKeyAsBytes() { + return Base64.getDecoder().decode(dto.getKey()); + } + + PublicKey getUserPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + KeyFactory kf = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME); + ECNamedCurveParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1"); + ECPoint point = ecSpec.getCurve().decodePoint(getKeyAsBytes()); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, ecSpec); + + return kf.generatePublic(pubSpec); + } + + String getEndpoint() { + return dto.getEndpoint(); + } +}