From 3ad259651c428a4abc6f6dd016369eb23b59ab83 Mon Sep 17 00:00:00 2001 From: MK13 Date: Thu, 23 Jul 2020 23:47:12 +0200 Subject: [PATCH] Stuff all over the place --- push-service-client-lib/pom.xml | 10 ++- push-service-client-lib/readme.txt | 15 ++++ .../client/PushServiceClientApplication.java | 21 +++++ .../config/PushServiceClientConfig.java | 18 ++++ .../controller/SubscriptionController.java | 29 +++++++ .../de/pushservice/client/dto/MessageDto.java | 6 ++ .../client/dto/NotificationRequestDto.java | 5 ++ .../de/pushservice/client/dto/PayloadDto.java | 63 ++++++++++++++ .../client/service/SubscriptionService.java | 66 +++++++++++++++ .../javascript/push-service-client-init.js | 82 +++++++++++++++++++ .../push-service-client-service-worker.js | 14 ++++ .../server/PushServiceApplication.java | 3 + .../controller/PushServiceController.java | 10 ++- .../resources/config/application.properties | 13 +++ 14 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 push-service-client-lib/readme.txt create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/PushServiceClientApplication.java create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/config/PushServiceClientConfig.java create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/controller/SubscriptionController.java create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/dto/PayloadDto.java create mode 100644 push-service-client-lib/src/main/java/de/pushservice/client/service/SubscriptionService.java create mode 100644 push-service-client-lib/src/main/resources/static/javascript/push-service-client-init.js create mode 100644 push-service-client-lib/src/main/resources/static/push-service-client-service-worker.js create mode 100644 push-service-server/src/main/resources/config/application.properties diff --git a/push-service-client-lib/pom.xml b/push-service-client-lib/pom.xml index e085ad3..d2b5d5b 100644 --- a/push-service-client-lib/pom.xml +++ b/push-service-client-lib/pom.xml @@ -24,6 +24,10 @@ org.springframework spring-web + + org.springframework.boot + spring-boot-starter-web + @@ -32,7 +36,11 @@ org.springframework.boot spring-boot-maven-plugin - true + + exec diff --git a/push-service-client-lib/readme.txt b/push-service-client-lib/readme.txt new file mode 100644 index 0000000..a47e019 --- /dev/null +++ b/push-service-client-lib/readme.txt @@ -0,0 +1,15 @@ +1. +====================== + + +2. How to use the client-lib in a client application +==================================================== +- Include property 'push-service-client.serverUrl=' and set it to the path to the server part +- (optional) Enable loggin via property 'logging.level.de.pushservice=DEBUG' +- Enable component scan in app configuration via '@ComponentScan("de.pushservice.client")' +- Obtain an instance of SubscriptionService via '@Autowired' +- Add a JavaScript file with content 'registerServiceWorker();' to register the service worker and subscribing at the + third party push service +- Add the created JavaScript file to HTML via '' +- Also add the PushService client JavaScript to HTML via + '' \ No newline at end of file diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/PushServiceClientApplication.java b/push-service-client-lib/src/main/java/de/pushservice/client/PushServiceClientApplication.java new file mode 100644 index 0000000..ac96a1c --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/PushServiceClientApplication.java @@ -0,0 +1,21 @@ +package de.pushservice.client; + +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; + +/** + * Not used but required by the spring boot maven plugin during repackage + */ +@SpringBootApplication +public class PushServiceClientApplication extends SpringBootServletInitializer { + public static void main(String[] args) { + SpringApplication.run(PushServiceClientApplication.class); + } + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(PushServiceClientApplication.class); + } +} \ No newline at end of file diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/config/PushServiceClientConfig.java b/push-service-client-lib/src/main/java/de/pushservice/client/config/PushServiceClientConfig.java new file mode 100644 index 0000000..7b40e75 --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/config/PushServiceClientConfig.java @@ -0,0 +1,18 @@ +package de.pushservice.client.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "push-service-client") +public class PushServiceClientConfig { + private String serverUrl; + + public String getServerUrl() { + return serverUrl; + } + + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } +} diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/controller/SubscriptionController.java b/push-service-client-lib/src/main/java/de/pushservice/client/controller/SubscriptionController.java new file mode 100644 index 0000000..8d63749 --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/controller/SubscriptionController.java @@ -0,0 +1,29 @@ +package de.pushservice.client.controller; + +import de.pushservice.client.ResponseReason; +import de.pushservice.client.dto.SubscriptionDto; +import de.pushservice.client.service.SubscriptionService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@Controller +public class SubscriptionController { + private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionController.class); + + @Autowired + private SubscriptionService subscriptionService; + + @PostMapping("/subscription/subscribe") + public ResponseEntity subscribe(@RequestBody SubscriptionDto subscriptionDto) { + LOGGER.debug(String.format("Received subscription for endpoint %s", subscriptionDto.getEndpoint())); + + this.subscriptionService.subscribe(subscriptionDto); + + return ResponseReason.OK.toResponseEntity(); + } +} 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 index 37f8105..464ee50 100644 --- 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 @@ -7,6 +7,12 @@ public class MessageDto { private String topic; private Urgency urgency; + public MessageDto(String payload, String topic, Urgency urgency) { + this.payload = payload; + this.topic = topic; + this.urgency = urgency; + } + public String getPayload() { return payload; } 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 index b808b45..4758416 100644 --- 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 @@ -4,6 +4,11 @@ public class NotificationRequestDto { private SubscriptionDto subscriptionDto; private MessageDto messageDto; + public NotificationRequestDto(SubscriptionDto subscriptionDto, MessageDto messageDto) { + this.subscriptionDto = subscriptionDto; + this.messageDto = messageDto; + } + public SubscriptionDto getSubscriptionDto() { return subscriptionDto; } diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/dto/PayloadDto.java b/push-service-client-lib/src/main/java/de/pushservice/client/dto/PayloadDto.java new file mode 100644 index 0000000..921343d --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/dto/PayloadDto.java @@ -0,0 +1,63 @@ +package de.pushservice.client.dto; + +/** + *

+ * Payload for a notification. Gets sent to the third party push service which forwards it to the user agent. + *

+ *

+ * Evaluated after receival in the push-service-client-service-worker.js service worker. + *

+ * + * @see MDN spec for notification payload + */ +public class PayloadDto { + private String title; + private String message; + private String icon; + private int[] vibration; + private long timestamp; + + public PayloadDto(String title) { + this.title = title; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public int[] getVibration() { + return vibration; + } + + public void setVibration(int[] vibration) { + this.vibration = vibration; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/push-service-client-lib/src/main/java/de/pushservice/client/service/SubscriptionService.java b/push-service-client-lib/src/main/java/de/pushservice/client/service/SubscriptionService.java new file mode 100644 index 0000000..8181686 --- /dev/null +++ b/push-service-client-lib/src/main/java/de/pushservice/client/service/SubscriptionService.java @@ -0,0 +1,66 @@ +package de.pushservice.client.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.pushservice.client.ResponseReason; +import de.pushservice.client.config.PushServiceClientConfig; +import de.pushservice.client.dto.MessageDto; +import de.pushservice.client.dto.NotificationRequestDto; +import de.pushservice.client.dto.PayloadDto; +import de.pushservice.client.dto.SubscriptionDto; +import de.pushservice.client.model.Urgency; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.*; + +@Service +public class SubscriptionService { + private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionService.class); + + @Autowired + private PushServiceClientConfig config; + + @Autowired + private ObjectMapper objectMapper; + + private Set subscriptions = new HashSet<>(); + + public void subscribe(SubscriptionDto subscriptionDto) { + this.subscriptions.add(subscriptionDto); + } + + public int notifyAll(PayloadDto payload, String topic, Urgency urgency) throws JsonProcessingException { + int notificationsSend = 0; + final Urgency tmpUrgency = Optional.ofNullable(urgency).orElse(Urgency.NORMAL); + + final MessageDto messageDto = new MessageDto(objectMapper.writeValueAsString(payload), topic, tmpUrgency); + + for (SubscriptionDto subscription : this.subscriptions) { + final NotificationRequestDto requestDto = new NotificationRequestDto(subscription, messageDto); + + LOGGER.debug(String.format("Sending notification for endpoint %s", subscription.getEndpoint())); + + final ResponseEntity responseEntity = new RestTemplate() + .exchange(this.config.getServerUrl(), HttpMethod.POST, new HttpEntity<>(requestDto), String.class); + + final ResponseReason responseReason = ResponseReason.fromResponseEntity(responseEntity); + + if (ResponseReason.OK == responseReason) { + notificationsSend++; + } + else { + // Well, nothing we can do about it + LOGGER.error("Sending notification to endpoint failed! %s", responseReason); + } + } + + return notificationsSend; + } +} diff --git a/push-service-client-lib/src/main/resources/static/javascript/push-service-client-init.js b/push-service-client-lib/src/main/resources/static/javascript/push-service-client-init.js new file mode 100644 index 0000000..7053990 --- /dev/null +++ b/push-service-client-lib/src/main/resources/static/javascript/push-service-client-init.js @@ -0,0 +1,82 @@ +function registerServiceWorker() { + if ('serviceWorker' in navigator) { + window.addEventListener('load', function() { + navigator.serviceWorker.register('push-service-client-service-worker.js').then(function(registration) { + // Registration was successful + console.log('ServiceWorker registration successful with scope: ', registration.scope); + + initializeState(); + }, function(err) { + // registration failed :( + console.log('ServiceWorker registration failed: ', err); + }); + }); + } +} + +function initializeState() { + if (!('showNotification' in ServiceWorkerRegistration.prototype)) { + console.warn('Notifications aren\'t supported.'); + return; + } + + if (Notification.permission === 'denied') { + console.warn('The user has blocked notifications.'); + return; + } + + if (!('PushManager' in window)) { + console.warn('Push messaging isn\'t supported.'); + return; + } + + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { + serviceWorkerRegistration.pushManager.getSubscription().then(function (subscription) { + if (!subscription) { + subscribe(); + return; + } + + sendSubscriptionToServer(subscription); + }) + .catch(function(err) { + console.warn('Error during getSubscription()', err); + }); + }); +} + +function subscribe() { + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { + serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true}).then(function (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); + } + }); + }); +} + +function sendSubscriptionToServer(subscription) { + var key = subscription.getKey ? subscription.getKey('p256dh') : ''; + var auth = subscription.getKey ? subscription.getKey('auth') : ''; + + console.log('Sending subscription to push-service-client'); + + return fetch('subscription/subscribe', { + 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-client-lib/src/main/resources/static/push-service-client-service-worker.js b/push-service-client-lib/src/main/resources/static/push-service-client-service-worker.js new file mode 100644 index 0000000..658be18 --- /dev/null +++ b/push-service-client-lib/src/main/resources/static/push-service-client-service-worker.js @@ -0,0 +1,14 @@ +// The worker cannot reside in javascript/ because of the scope policy for service workers defined in the spec + +self.addEventListener('push', function(event) { + const payload = event.data.json(); + + event.waitUntil( + self.registration.showNotification(payload.title, { + body: payload.message, + icon: payload.icon, + vibrate: payload.vibration, + timestamp: payload.timestamp, + }) + ); +}); \ 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 index 26e4175..e2bdeea 100644 --- a/push-service-server/src/main/java/de/pushservice/server/PushServiceApplication.java +++ b/push-service-server/src/main/java/de/pushservice/server/PushServiceApplication.java @@ -5,6 +5,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +/** + * This whole application is basically just a thin layer around the webpush-java lib + */ @SpringBootApplication public class PushServiceApplication extends SpringBootServletInitializer { public static void main(String[] args) { 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 index e06ba8b..9f91079 100644 --- 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 @@ -6,18 +6,26 @@ import de.pushservice.server.decorator.MessageDecorator; import de.pushservice.server.decorator.SubscriptionDecorator; import nl.martijndwars.webpush.Notification; import nl.martijndwars.webpush.PushService; +import org.bouncycastle.jce.provider.BouncyCastleProvider; 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.RequestBody; import org.springframework.web.bind.annotation.RestController; +import java.security.Security; + @RestController public class PushServiceController { private static final Logger LOGGER = LoggerFactory.getLogger(PushServiceController.class); + static { + Security.addProvider(new BouncyCastleProvider()); + } + @PostMapping - public ResponseEntity notify(NotificationRequestDto notificationRequestDto) { + public ResponseEntity notify(@RequestBody NotificationRequestDto notificationRequestDto) { try { final SubscriptionDecorator subscription = SubscriptionDecorator .fromDto(notificationRequestDto.getSubscriptionDto()); diff --git a/push-service-server/src/main/resources/config/application.properties b/push-service-server/src/main/resources/config/application.properties new file mode 100644 index 0000000..4a7a510 --- /dev/null +++ b/push-service-server/src/main/resources/config/application.properties @@ -0,0 +1,13 @@ +server.servlet.context-path=/push-service-server +server.port=8077 + +info.app.name=PushService server +info.app.description=A simple server for webpush +info.build.group=@project.groupId@ +info.build.artifact=@project.artifactId@ +info.build.version=@project.version@ + +logging.level.de.pushservice=DEBUG +logging.file=push-service-server.log +logging.file.max-history=7 +logging.file.max-size=50MB \ No newline at end of file