diff --git a/pom.xml b/pom.xml index 00ce236..aa55b91 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,11 @@ bcprov-jdk15on 1.64 + + org.bouncycastle + bcpkix-jdk15on + 1.64 + org.apache.commons commons-collections4 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 index 9ed5eb1..dc0c5b2 100644 --- 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 @@ -6,11 +6,12 @@ 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.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; -@Controller +@RestController public class SubscriptionController { private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionController.class); @@ -23,4 +24,9 @@ public class SubscriptionController { return this.subscriptionService.subscribe(subscriptionDto).toResponseEntity(); } + + @GetMapping("subscription/vapidPublicKey") + public String getVapidPublicKey() { + return this.subscriptionService.getVapidPublicKey(); + } } 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 464ee50..a1ff883 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 @@ -1,6 +1,7 @@ package de.pushservice.client.dto; import de.pushservice.client.model.Urgency; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; public class MessageDto { private String payload; @@ -36,4 +37,9 @@ public class MessageDto { public void setUrgency(Urgency urgency) { this.urgency = urgency; } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } } 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 daaa81f..2e3c6ad 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 @@ -1,5 +1,7 @@ package de.pushservice.client.dto; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; + public class NotificationRequestDto { private String scope; private MessageDto messageDto; @@ -24,4 +26,9 @@ public class NotificationRequestDto { public void setMessageDto(MessageDto messageDto) { this.messageDto = messageDto; } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } } 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 index 921343d..978d62b 100644 --- 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 @@ -1,5 +1,7 @@ package de.pushservice.client.dto; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; + /** *

* Payload for a notification. Gets sent to the third party push service which forwards it to the user agent. @@ -60,4 +62,9 @@ public class PayloadDto { public void setTitle(String title) { this.title = title; } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } } 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 index 9093064..0539c4a 100644 --- 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 @@ -1,5 +1,7 @@ package de.pushservice.client.dto; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; + public class SubscriptionDto { private String auth; private String endpoint; @@ -37,4 +39,9 @@ public class SubscriptionDto { public void setScope(String scope) { this.scope = scope; } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } } 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 index 07d1b01..23e1203 100644 --- 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 @@ -68,4 +68,11 @@ public class SubscriptionService { LOGGER.error(String.format("Could not send notification! %s", responseReason)); } } + + public String getVapidPublicKey() { + final ResponseEntity responseEntity = new RestTemplate() + .exchange(this.config.getServerUrl() + "vapidPublicKey", HttpMethod.GET, HttpEntity.EMPTY, String.class); + + return responseEntity.getBody(); + } } 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 index b68587e..280147a 100644 --- 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 @@ -45,9 +45,20 @@ function initializeState(registrationScope) { }); } -function subscribe(registrationScope) { +async function subscribe(registrationScope) { + let response = await fetch('subscription/vapidPublicKey'); + let key = await response.text(); + options = {}; + + if (!key) { + options = {userVisibleOnly: true}; + } + else { + options = {userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array(key)}; + } + navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { - serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true}).then(function (subscription) { + serviceWorkerRegistration.pushManager.subscribe(options).then(function (subscription) { return sendSubscriptionToServer(subscription, registrationScope); }) .catch(function (e) { @@ -80,4 +91,20 @@ function sendSubscriptionToServer(subscription, registrationScope) { scope: registrationScope }) }); +} + +function base64UrlToUint8Array(base64UrlData) { + const padding = '='.repeat((4 - base64UrlData.length % 4) % 4); + const base64 = (base64UrlData + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = atob(base64); + const buffer = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + buffer[i] = rawData.charCodeAt(i); + } + + return buffer; } \ No newline at end of file diff --git a/push-service-server/pom.xml b/push-service-server/pom.xml index c931755..e2d64dd 100644 --- a/push-service-server/pom.xml +++ b/push-service-server/pom.xml @@ -45,6 +45,10 @@ org.bouncycastle bcprov-jdk15on + + org.bouncycastle + bcpkix-jdk15on + org.flywaydb flyway-core diff --git a/push-service-server/src/main/java/de/pushservice/server/config/PushServiceServerConfig.java b/push-service-server/src/main/java/de/pushservice/server/config/PushServiceServerConfig.java new file mode 100644 index 0000000..97a6915 --- /dev/null +++ b/push-service-server/src/main/java/de/pushservice/server/config/PushServiceServerConfig.java @@ -0,0 +1,39 @@ +package de.pushservice.server.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "push-service-server") +public class PushServiceServerConfig { + public static class Vapid { + private String publicKeyPath; + private String privateKeyPath; + + public String getPublicKeyPath() { + return publicKeyPath; + } + + public void setPublicKeyPath(String publicKeyPath) { + this.publicKeyPath = publicKeyPath; + } + + public String getPrivateKeyPath() { + return privateKeyPath; + } + + public void setPrivateKeyPath(String privateKeyPath) { + this.privateKeyPath = privateKeyPath; + } + } + + private Vapid vapid; + + public Vapid getVapid() { + return vapid; + } + + public void setVapid(Vapid vapid) { + this.vapid = vapid; + } +} 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 ded2296..4593a17 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 @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -22,16 +23,29 @@ public class PushServiceController { private PushService pushService; static { - Security.addProvider(new BouncyCastleProvider()); + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } } @PostMapping("/notify") public ResponseEntity notify(@RequestBody NotificationRequestDto notificationRequestDto) { + LOGGER.debug(String.format("notify called with %s", notificationRequestDto)); + return this.pushService.notify(notificationRequestDto).toResponseEntity(); } @PostMapping("/subscribe") public ResponseEntity subscribe(@RequestBody SubscriptionDto subscriptionDto) { + LOGGER.debug(String.format("subscribe called with %s", subscriptionDto)); + return this.pushService.subscribe(subscriptionDto).toResponseEntity(); } + + @GetMapping("/vapidPublicKey") + public String getVapidPublicKey() { + LOGGER.debug("vapidPublicKey called"); + + return this.pushService.getVapidPublicKey(); + } } diff --git a/push-service-server/src/main/java/de/pushservice/server/service/PushService.java b/push-service-server/src/main/java/de/pushservice/server/service/PushService.java index d042408..fc311eb 100644 --- a/push-service-server/src/main/java/de/pushservice/server/service/PushService.java +++ b/push-service-server/src/main/java/de/pushservice/server/service/PushService.java @@ -4,12 +4,18 @@ import de.pushservice.client.ResponseReason; import de.pushservice.client.dto.MessageDto; import de.pushservice.client.dto.NotificationRequestDto; import de.pushservice.client.dto.SubscriptionDto; +import de.pushservice.server.config.PushServiceServerConfig; import de.pushservice.server.dba.SubscriptionRepository; import de.pushservice.server.decorator.SubscriptionDecorator; import de.pushservice.server.model.Subscription; import nl.martijndwars.webpush.Notification; import nl.martijndwars.webpush.Urgency; +import org.apache.commons.collections4.IterableUtils; import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -17,7 +23,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.security.KeyPair; import java.time.LocalDateTime; +import java.util.Base64; +import java.util.Optional; @Service public class PushService { @@ -29,10 +41,21 @@ public class PushService { @Autowired private ExternalPushServiceResponseHandler handler; + @Autowired + private PushServiceServerConfig config; + @Transactional(propagation = Propagation.REQUIRED) public ResponseReason subscribe(SubscriptionDto subscriptionDto) { - if (this.subscriptionRepository - .getForScopeAndEndpoint(subscriptionDto.getScope(), subscriptionDto.getEndpoint()).isPresent()) { + Optional optSubscription = this.subscriptionRepository + .getForScopeAndEndpoint(subscriptionDto.getScope(), subscriptionDto.getEndpoint()); + + if (optSubscription.isPresent()) { + final Subscription subscription = optSubscription.get(); + + subscription.setLastSeen(LocalDateTime.now()); + + this.subscriptionRepository.save(subscription); + return ResponseReason.OK; } @@ -58,9 +81,14 @@ public class PushService { final Iterable subscriptions = this.subscriptionRepository .getAllForScope(notificationRequestDto.getScope()); - final nl.martijndwars.webpush.PushService extPushService = new nl.martijndwars.webpush.PushService(); + + LOGGER.debug(String.format("Found %s subscriptions for scope %s", IterableUtils.size(subscriptions), notificationRequestDto.getScope())); + + final nl.martijndwars.webpush.PushService extPushService = new nl.martijndwars.webpush.PushService(getKeyPair()); for (Subscription subscription : subscriptions) { + LOGGER.debug(String.format("Process endpoint %s", subscription.getEndpoint())); + try { SubscriptionDecorator subscriptionDecorator = SubscriptionDecorator.from(subscription); Notification notification = Notification.builder() @@ -87,4 +115,51 @@ public class PushService { private Urgency getExtUrgency(MessageDto messageDto) { return Urgency.valueOf(messageDto.getUrgency().name()); } + + public String getVapidPublicKey() { + if (StringUtils.isEmpty(this.config.getVapid().getPublicKeyPath())) { + return null; + } + + final File publicKeyFile = new File(this.config.getVapid().getPublicKeyPath()); + + if (!publicKeyFile.exists()) { + return null; + } + + try (FileReader fr = new FileReader(publicKeyFile)) { + PEMParser pemParser = new PEMParser(fr); + SubjectPublicKeyInfo keyInfo = (SubjectPublicKeyInfo) pemParser.readObject(); + + return Base64.getEncoder().encodeToString(keyInfo.getPublicKeyData().getBytes()); + } + catch (IOException ioe) { + LOGGER.error(String.format("Could not read key! %s", this.config.getVapid().getPublicKeyPath()), ioe); + + return null; + } + } + + private KeyPair getKeyPair() { + if (StringUtils.isEmpty(this.config.getVapid().getPrivateKeyPath())) { + return null; + } + + final File publicKeyFile = new File(this.config.getVapid().getPrivateKeyPath()); + + if (!publicKeyFile.exists()) { + return null; + } + + try (FileReader fr = new FileReader(publicKeyFile)) { + PEMParser pemParser = new PEMParser(fr); + + return new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) pemParser.readObject()); + } + catch (IOException ioe) { + LOGGER.error(String.format("Could not read key! %s", this.config.getVapid().getPrivateKeyPath()), ioe); + + return null; + } + } } diff --git a/push-service-server/src/main/resources/config/application-mk.properties b/push-service-server/src/main/resources/config/application-mk.properties index e9e2a59..c5f31e0 100644 --- a/push-service-server/src/main/resources/config/application-mk.properties +++ b/push-service-server/src/main/resources/config/application-mk.properties @@ -1,3 +1,6 @@ spring.datasource.url=jdbc:postgresql://localhost/push_service_mk spring.datasource.username=push_service_mk -spring.datasource.password=push_service_mk \ No newline at end of file +spring.datasource.password=push_service_mk + +push-service-server.vapid.public-key-path=/opt/push-service/mk/vapid_public.pem +push-service-server.vapid.private-key-path=/opt/push-service/mk/vapid_private.pem \ No newline at end of file diff --git a/push-service-server/src/main/resources/config/application.properties b/push-service-server/src/main/resources/config/application.properties index 27a4649..6cee8ff 100644 --- a/push-service-server/src/main/resources/config/application.properties +++ b/push-service-server/src/main/resources/config/application.properties @@ -18,4 +18,8 @@ logging.file.max-size=50MB # because the connection pool cannot shutdown properly spring.jmx.enabled=false -spring.jpa.hibernate.ddl-auto=validate \ No newline at end of file +spring.jpa.hibernate.ddl-auto=validate + +# Follow https://github.com/web-push-libs/webpush-java/wiki/VAPID how to create the keys +push-service-server.vapid.public-key-path=/tmp/vapid_public.pem +push-service-server.vapid.private-key-path=/tmp/vapid_private.pem \ No newline at end of file