A Java Web Push server-side library that can easily be integrated with various third-party libraries and frameworks.
This library
- Provides the functionalities for VAPID
- Provides the functionalities for Message Encryption for Web Push
- Assumes that the Push API is used
zerodep-web-push-java | java version requirements |
---|---|
v2.x.x | java 11 or higher |
v1.x.x | java 8 or higher |
It is recommended that you use v2.x.x (the latest version of v2) if you can use java 11 or higher. Some features are only available in version 2.
The documentation specific to v1 is here.
<dependency>
<groupId>com.zerodeplibs</groupId>
<artifactId>zerodep-web-push-java</artifactId>
<version>2.1.2</version>
</dependency>
Sending push notifications requires slightly complex steps. So it is recommended that you check one of the example projects(Please see Examples).
The following is a typical flow to send push notifications with this library.
-
Generate a key pair for VAPID with an arbitrary way(e.g. openssl commands).
Example:
openssl ecparam -genkey -name prime256v1 -noout -out sourceKey.pem openssl pkcs8 -in sourceKey.pem -topk8 -nocrypt -out vapidPrivateKey.pem openssl ec -in sourceKey.pem -pubout -conv_form uncompressed -out vapidPublicKey.pem
-
Instantiate
VAPIDKeyPair
with the key pair generated in '1.'.Example:
VAPIDKeyPair vapidKeyPair = VAPIDKeyPairs.of( PrivateKeySources.ofPEMFile(new File(pathToYourPrivateKeyFile).toPath()), PublicKeySources.ofPEMFile(new File(pathToYourPublicKeyFile).toPath() );
-
Send the public key for VAPID to the browser.
Typically, this is achieved by exposing an endpoint to get the public key like
GET /getPublicKey
. Javascript on the browser fetches the public key through this endpoint.Example:
@GetMapping("/getPublicKey") public byte[] getPublicKey() { return vapidKeyPair.extractPublicKeyInUncompressedForm(); }
(javascript on browser)
const serverPublicKey = await fetch('/getPublicKey') .then(response => response.arrayBuffer()); const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: serverPublicKey });
-
Obtain a push subscription from the browser.
Typically, this is achieved by exposing an endpoint for the browser to post the push subscription like
POST /subscribe
.Example:
@PostMapping("/subscribe") public void subscribe(@RequestBody PushSubscription subscription) { this.saveSubscriptionToStorage(subscription); }
(javascript on browser)
await fetch('/subscribe', { method: 'POST', body: JSON.stringify(subscription), headers: { 'content-type': 'application/json' } }).then(res => { ..... });
-
Send a push notification to the push service by using RequestPreparer (e.g.
StandardHttpClientRequestPreparer
) with theVAPIDKeyPair
and the push subscription.HttpRequest request = StandardHttpClientRequestPreparer.getBuilder() .pushSubscription(subscription) .vapidJWTExpiresAfter(15, TimeUnit.MINUTES) .vapidJWTSubject("mailto:example@example.com") .pushMessage(message) .ttl(1, TimeUnit.HOURS) .urgencyLow() .topic("MyTopic") .build(vapidKeyPair) .toRequest(); HttpResponse<String> httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Source code and usage: zerodep-web-push-java-example
Controller for VAPID and Message Encryption
@Component
public class MyComponents {
/**
* In this example, we read a key pair for VAPID
* from a PEM formatted file on the file system.
* <p>
* You can extract key pairs from various sources:
* '.der' file(binary content), an octet sequence stored in a database and so on.
* For more information, please see the javadoc of PrivateKeySources and PublicKeySources.
*/
@Bean
public VAPIDKeyPair vaidKeyPair(
@Value("${private.key.file.path}") String privateKeyFilePath,
@Value("${public.key.file.path}") String publicKeyFilePath) throws IOException {
return VAPIDKeyPairs.of(
PrivateKeySources.ofPEMFile(new File(privateKeyFilePath).toPath()),
PublicKeySources.ofPEMFile(new File(publicKeyFilePath).toPath())
);
}
}
@SpringBootApplication
@RestController
public class BasicExample {
/**
* @see MyComponents
*/
@Autowired
private VAPIDKeyPair vapidKeyPair;
/**
* # Step 1.
* Sends the public key to user agents.
* <p>
* The user agents create a push subscription with this public key.
*/
@GetMapping("/getPublicKey")
public byte[] getPublicKey() {
return vapidKeyPair.extractPublicKeyInUncompressedForm();
}
/**
* # Step 2.
* Obtains push subscriptions from user agents.
* <p>
* The application server(this application) requests the delivery of push messages with these subscriptions.
*/
@PostMapping("/subscribe")
public void subscribe(@RequestBody PushSubscription subscription) {
this.saveSubscriptionToStorage(subscription);
}
/**
* # Step 3.
* Requests the delivery of push messages.
* <p>
* In this example, for simplicity and testability, we use an HTTP endpoint for this purpose.
* However, in real applications, this feature doesn't have to be provided as an HTTP endpoint.
*/
@PostMapping("/sendMessage")
public ResponseEntity<String> sendMessage(@RequestBody MyMessage myMessage)
throws IOException, InterruptedException {
String message = myMessage.getMessage();
HttpClient httpClient = HttpClient.newBuilder().build();
for (PushSubscription subscription : getSubscriptionsFromStorage()) {
HttpRequest request = StandardHttpClientRequestPreparer.getBuilder()
.pushSubscription(subscription)
.vapidJWTExpiresAfter(15, TimeUnit.MINUTES)
.vapidJWTSubject("mailto:example@example.com")
.pushMessage(message)
.ttl(1, TimeUnit.HOURS)
.urgencyLow()
.topic("MyTopic")
.build(vapidKeyPair)
.toRequest();
// In this example, we send push messages in simple text format.
// You can also send them in JSON format as follows:
//
// ObjectMapper objectMapper = (Create a new one or get from the DI container.)
// ....
// .pushMessage(objectMapper.writeValueAsBytes(objectForJson))
// ....
HttpResponse<String> httpResponse =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info(String.format("[Http Client] status code: %d", httpResponse.statusCode()));
// 201 Created : Success!
// 410 Gone : The subscription is no longer valid.
// etc...
// for more information, see the useful link below:
// [Response from push service - The Web Push Protocol ](https://developers.google.com/web/fundamentals/push-notifications/web-push-protocol)
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
.body("The message has been processed.");
}
... Omitted for simplicity.
}
Source code and usage: zerodep-web-push-java-example-webflux
Source code and usage: zerodep-web-push-java-example-vertx
Standalone application for VAPID and Message Encryption
public class Example {
/**
* In this example, we read a key pair for VAPID
* from a PEM formatted file on the file system.
* <p>
* You can extract key pairs from various sources:
* '.der' file(binary content), an octet sequence stored in a database and so on.
* For more information, please see the javadoc of PrivateKeySources and PublicKeySources.
*/
private static VAPIDKeyPair createVAPIDKeyPair(Vertx vertx) throws IOException {
return VAPIDKeyPairs.of(
PrivateKeySources.ofPEMFile(new File("./.keys/my-private_pkcs8.pem").toPath()),
PublicKeySources.ofPEMFile(new File("./.keys/my-pub.pem").toPath()),
new VertxVAPIDJWTGeneratorFactory(() -> vertx));
}
public static void main(String[] args) throws IOException {
Vertx vertx = Vertx.vertx();
WebClient client = WebClient.create(vertx);
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
VAPIDKeyPair vapidKeyPair = createVAPIDKeyPair(vertx);
MockSubscriptionStorage mockStorage = new MockSubscriptionStorage();
/*
* # Step 1.
* Sends the public key to user agents.
*
* The user agents create a push subscription with this public key.
*/
router
.get("/getPublicKey")
.handler(ctx ->
ctx.response()
.putHeader("Content-Type", "application/octet-stream")
.end(Buffer.buffer(vapidKeyPair.extractPublicKeyInUncompressedForm()))
);
/*
* # Step 2.
* Obtains push subscriptions from user agents.
*
* The application server(this application) requests the delivery of push messages with these subscriptions.
*/
router
.post("/subscribe")
.handler(ctx -> {
PushSubscription subscription =
ctx.getBodyAsJson().mapTo(PushSubscription.class);
mockStorage.saveSubscriptionToStorage(subscription);
ctx.response().end();
});
/*
* # Step 3.
* Requests the delivery of push messages.
*
* In this example, for simplicity and testability, we use an HTTP endpoint for this purpose.
* However, in real applications, this feature doesn't have to be provided as an HTTP endpoint.
*/
router
.post("/sendMessage")
.handler(ctx -> {
String message = ctx.getBodyAsJson().getString("message");
vertx.getOrCreateContext().put("messageToSend", new SampleMessageData(message));
ExamplePushMessageDeliveryRequestProcessor processor =
new ExamplePushMessageDeliveryRequestProcessor(
vertx,
client,
vapidKeyPair,
mockStorage.getSubscriptionsFromStorage()
);
processor.start();
ctx.response()
.putHeader("Content-Type", "text/plain")
.end("Started sending notifications.");
});
router.route("/*").handler(StaticHandler.create());
vertx.createHttpServer().requestHandler(router).listen(8080, res -> {
System.out.println("Vert.x HTTP server started.");
});
}
/**
* Sends HTTP requests to push services to request the delivery of push messages.
* <p>
* This class utilizes:
* <ul>
* <li>{@link Vertx#executeBlocking(Handler, Handler)} for the JWT creation and the message encryption.</li>
* <li>{@link WebClient} for sending HTTP request asynchronously.</li>
* </ul>
*/
static class ExamplePushMessageDeliveryRequestProcessor {
private final Vertx vertx;
private final WebClient client;
private final VAPIDKeyPair vapidKeyPair;
private final List<PushSubscription> targetSubscriptions;
private final int requestIntervalMillis;
private final int connectionTimeoutMillis;
ExamplePushMessageDeliveryRequestProcessor(
Vertx vertx,
WebClient client,
VAPIDKeyPair vapidKeyPair,
Collection<PushSubscription> targetSubscriptions) {
this.vertx = vertx;
this.client = client;
this.vapidKeyPair = vapidKeyPair;
this.targetSubscriptions = targetSubscriptions.stream().collect(Collectors.toList());
this.requestIntervalMillis = 100;
this.connectionTimeoutMillis = 10_000;
}
void start() {
startInternal(0);
}
private void startInternal(int currentIndex) {
PushSubscription subscription = targetSubscriptions.get(currentIndex);
SampleMessageData messageData = vertx.getOrCreateContext().get("messageToSend");
vertx.executeBlocking(promise -> {
// In some circumstances, the JWT creation and the message encryption
// may be considered "blocking" operations.
//
// On the author's environment, the JWT creation takes about 0.7ms
// and the message encryption takes about 1.7ms.
//
// reference: https://vertx.io/docs/vertx-core/java/#golden_rule
VertxWebClientRequestPreparer requestPreparer =
VertxWebClientRequestPreparer.getBuilder()
.pushSubscription(subscription)
.vapidJWTExpiresAfter(15, TimeUnit.MINUTES)
.vapidJWTSubject("mailto:example@example.com")
.pushMessage(messageData.getMessage())
.ttl(1, TimeUnit.HOURS)
.urgencyNormal()
.topic("MyTopic")
.build(vapidKeyPair);
promise.complete(requestPreparer);
}, res -> {
VertxWebClientRequestPreparer requestPreparer =
(VertxWebClientRequestPreparer) res.result();
requestPreparer.sendBuffer(
client,
req -> req.timeout(connectionTimeoutMillis),
httpResponseAsyncResult -> {
HttpResponse<Buffer> result = httpResponseAsyncResult.result();
System.out.println(
String.format("status code: %d", result.statusCode()));
// 201 Created : Success!
// 410 Gone : The subscription is no longer valid.
// etc...
// for more information, see the useful link below:
// [Response from push service - The Web Push Protocol ](https://developers.google.com/web/fundamentals/push-notifications/web-push-protocol)
}
);
});
if (currentIndex == targetSubscriptions.size() - 1) {
return;
}
// In order to avoid wasting bandwidth,
// we send HTTP requests at some intervals.
vertx.setTimer(requestIntervalMillis, id -> startInternal(currentIndex + 1));
}
}
... Omitted for simplicity.
}
'zerodep-web-push-java' assumes that suitable implementations(libraries) of the following functionalities vary depending on applications.
- Generating and signing JSON Web Token(JWT) used for VAPID
- Sending HTTP requests for the delivery of push messages
- Cryptographic operations
For example, an application may need to send HTTP requests synchronously with Apache HTTPClient but another application may need to do this asynchronously with Vert.x.
In order to allow you to choose the way suitable for your application, this library doesn't force your application to have dependencies on specifics libraries. Instead, this library
- Provides the functionality of JWT for VAPID with sub-modules
- Also, provides the functionality of JWT for VAPID out of the box(without any third-party library)
- Provides optional components helping applications use various third-party HTTP Client libraries
- Also, provides a component helping applications use JDK's HTTP Client module.
- Utilizes the Java Cryptography Architecture (JCA) for cryptographic operations
Each of the sub-modules utilizes a specific JWT library. Each of the optional components supports a specific HTTP Client library. you can choose suitable modules/components for your requirements. JCA enables this library to be independent of specific implementations(providers) for security functionality.
The following functionalities can be provided from outside this library.
JWT
JWT libraries are used to generate JSON Web Token (JWT) for VAPID.
Sub-modules for this functionality are available from zerodep-web-push-java-ext-jwt.
These sub-modules are optional.
HTTP Client
Application servers need to send HTTP requests to push services in order to request the delivery of
push messages. Helper components for this functionality are available from
the com.zerodeplibs.webpush.httpclient
package. One of them
utilizes JDK's HTTP Client module
.
The others utilize third-party HTTP Client libraries. Supported third-party libraries are listed
below.
-
Version 4.9.0 or higher. The latest version is recommended.
-
Version 5.1 or higher. The latest version is recommended.
-
Eclipse Jetty Client Libraries
- Jetty 9: 9.4.33.v20201020 or higher.
- Jetty 10: 10.0.0 or higher.
- Jetty 11: 11.0.0 or higher.
The latest versions are recommended.
-
- Vert.x 3: 3.9.2 or higher.
- Vert.x 4: 4.0.0 or higher.
The latest versions are recommended.
-
Others
'zerodep-web-push-java' doesn't directly provide optional components for the libraries other than the above. However, 'zerodep-web-push-java' can be easily integrated with the other HTTP Client libraries and frameworks. For example, you can also utilize the following libraries.
Please see zerodep-web-push-java-example-webflux for more information.
Null safety
The public methods and constructors of this library do not accept null
s and do not return null
s.
They throw an Exception
if a null reference is passed. Some methods
return java.util.Optional.empty()
if they need to indicate that the value does not exist.
The exceptions are:
com.zerodeplibs.webpush.PushSubscription.java
. This is the server-side representation of push subscription.- The methods of
Exception
. For example, theirgetCause()
can return null.
Working with Java Cryptography Architecture(JCA)
This library uses the Java Cryptography Architecture (JCA) API for cryptographic operations. The algorithms used by this library are listed below.
java.security.SecureRandom
java.security.KeyFactory.getInstance("EC")
java.security.KeyPairGenerator.getInstance("EC") // curve: secp256r1
java.security.Signature.getInstance("SHA256withECDSA")
javax.crypto.KeyAgreement.getInstance("ECDH")
javax.crypto.Mac.getInstance("HmacSHA256")
javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
By default, the providers shipped with the JDK will be used(e.g. SunEC
and SunJCE
).
Of course, any provider that supports these algorithms is available( e.g. Bouncy Castle). This is because 'zerodep-web-push-java' has no dependencies on any specific provider.
MIT
This project follows a git flow -style model.
Please open pull requests against the dev
branch.