headers = new HashMap<>();
+ byte[] body = null;
+
+ headers.put("TTL", String.valueOf(notification.getTTL()));
+
+ if (notification.hasUrgency()) {
+ headers.put("Urgency", notification.getUrgency().getHeaderValue());
+ }
+
+ if (notification.hasTopic()) {
+ headers.put("Topic", notification.getTopic());
+ }
+
+
+ if (notification.hasPayload()) {
+ headers.put("Content-Type", "application/octet-stream");
+
+ if (encoding == Encoding.AES128GCM) {
+ headers.put("Content-Encoding", "aes128gcm");
+ } else if (encoding == Encoding.AESGCM) {
+ headers.put("Content-Encoding", "aesgcm");
+ headers.put("Encryption", "salt=" + Base64.getUrlEncoder().withoutPadding().encodeToString(salt));
+ headers.put("Crypto-Key", "dh=" + Base64.getUrlEncoder().withoutPadding().encodeToString(dh));
+ }
+
+ body = encrypted.getCiphertext();
+ }
+
+ if (notification.isGcm()) {
+ if (getGcmApiKey() == null) {
+ throw new IllegalStateException("An GCM API key is needed to send a push notification to a GCM endpoint.");
+ }
+
+ headers.put("Authorization", "key=" + getGcmApiKey());
+ } else if (vapidEnabled()) {
+ if (encoding == Encoding.AES128GCM) {
+ if (notification.getEndpoint().startsWith("https://fcm.googleapis.com")) {
+ url = notification.getEndpoint().replace("fcm/send", "wp");
+ }
+ }
+
+ JwtClaims claims = new JwtClaims();
+ claims.setAudience(notification.getOrigin());
+ claims.setExpirationTimeMinutesInTheFuture(12 * 60);
+ if (getSubject() != null) {
+ claims.setSubject(getSubject());
+ }
+
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setHeader("typ", "JWT");
+ jws.setHeader("alg", "ES256");
+ jws.setPayload(claims.toJson());
+ jws.setKey(getPrivateKey());
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
+
+ byte[] pk = Utils.encode((ECPublicKey) getPublicKey());
+
+ if (encoding == Encoding.AES128GCM) {
+ headers.put("Authorization", "vapid t=" + jws.getCompactSerialization() + ", k=" + Base64.getUrlEncoder().withoutPadding().encodeToString(pk));
+ } else if (encoding == Encoding.AESGCM) {
+ headers.put("Authorization", "WebPush " + jws.getCompactSerialization());
+ }
+
+ if (headers.containsKey("Crypto-Key")) {
+ headers.put("Crypto-Key", headers.get("Crypto-Key") + ";p256ecdsa=" + Base64.getUrlEncoder().withoutPadding().encodeToString(pk));
+ } else {
+ headers.put("Crypto-Key", "p256ecdsa=" + Base64.getUrlEncoder().withoutPadding().encodeToString(pk));
+ }
+ } else if (notification.isFcm() && getGcmApiKey() != null) {
+ headers.put("Authorization", "key=" + getGcmApiKey());
+ }
+
+ return new HttpRequest(url, headers, body);
+ }
+
+ /**
+ * Set the Google Cloud Messaging (GCM) API key
+ *
+ * @param gcmApiKey
+ * @return
+ */
+ public T setGcmApiKey(String gcmApiKey) {
+ this.gcmApiKey = gcmApiKey;
+
+ return (T) this;
+ }
+
+ public String getGcmApiKey() {
+ return gcmApiKey;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ /**
+ * Set the JWT subject (for VAPID)
+ *
+ * @param subject
+ * @return
+ */
+ public T setSubject(String subject) {
+ this.subject = subject;
+
+ return (T) this;
+ }
+
+ /**
+ * Set the public and private key (for VAPID).
+ *
+ * @param keyPair
+ * @return
+ */
+ public T setKeyPair(KeyPair keyPair) {
+ setPublicKey(keyPair.getPublic());
+ setPrivateKey(keyPair.getPrivate());
+
+ return (T) this;
+ }
+
+ public PublicKey getPublicKey() {
+ return publicKey;
+ }
+
+ /**
+ * Set the public key using a base64url-encoded string.
+ *
+ * @param publicKey
+ * @return
+ */
+ public T setPublicKey(String publicKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+ setPublicKey(Utils.loadPublicKey(publicKey));
+
+ return (T) this;
+ }
+
+ public PrivateKey getPrivateKey() {
+ return privateKey;
+ }
+
+ public KeyPair getKeyPair() {
+ return new KeyPair(publicKey, privateKey);
+ }
+
+ /**
+ * Set the public key (for VAPID)
+ *
+ * @param publicKey
+ * @return
+ */
+ public T setPublicKey(PublicKey publicKey) {
+ this.publicKey = publicKey;
+
+ return (T) this;
+ }
+
+ /**
+ * Set the public key using a base64url-encoded string.
+ *
+ * @param privateKey
+ * @return
+ */
+ public T setPrivateKey(String privateKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+ setPrivateKey(Utils.loadPrivateKey(privateKey));
+
+ return (T) this;
+ }
+
+ /**
+ * Set the private key (for VAPID)
+ *
+ * @param privateKey
+ * @return
+ */
+ public T setPrivateKey(PrivateKey privateKey) {
+ this.privateKey = privateKey;
+
+ return (T) this;
+ }
+
+ /**
+ * Check if VAPID is enabled
+ *
+ * @return
+ */
+ protected boolean vapidEnabled() {
+ return publicKey != null && privateKey != null;
+ }
+}
diff --git a/src/main/java/nl/martijndwars/webpush/Base64Encoder.java b/src/main/java/nl/martijndwars/webpush/Base64Encoder.java
deleted file mode 100644
index b9f05ee..0000000
--- a/src/main/java/nl/martijndwars/webpush/Base64Encoder.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package nl.martijndwars.webpush;
-
-
-import org.apache.commons.codec.binary.Base64;
-
-/**
- * Java 7 compatible Base64 encode/decode functions. Based on Apache Commons Codec.
- *
- *
- * Note: Once upgrading to Java 8+, replace by native Base64 encoder.
- *
- */
-public class Base64Encoder {
-
- public static byte[] decode(String base64Encoded) {
- return Base64.decodeBase64(base64Encoded);
- }
-
- public static String encodeWithoutPadding(byte[] bytes) {
- return unpad(Base64.encodeBase64String(bytes));
- }
-
- public static String encodeUrl(byte[] bytes) {
- return pad(Base64.encodeBase64URLSafeString(bytes));
- }
-
- public static String encodeUrlWithoutPadding(byte[] bytes) {
- return Base64.encodeBase64URLSafeString(bytes);
- }
-
- private static String pad(String base64Encoded) {
- int m = base64Encoded.length() % 4;
- if (m == 2) {
- return base64Encoded + "==";
- } else if (m == 3) {
- return base64Encoded + "=";
- } else {
- return base64Encoded;
- }
- }
-
- private static String unpad(String base64Encoded) {
- if (base64Encoded.endsWith("==")) {
- return base64Encoded.substring(0, base64Encoded.length() - 2);
- } else if (base64Encoded.endsWith("=")) {
- return base64Encoded.substring(0, base64Encoded.length() - 1);
- } else {
- return base64Encoded;
- }
- }
-}
diff --git a/src/main/java/nl/martijndwars/webpush/HttpEce.java b/src/main/java/nl/martijndwars/webpush/HttpEce.java
index 4aacdc0..b9e9436 100644
--- a/src/main/java/nl/martijndwars/webpush/HttpEce.java
+++ b/src/main/java/nl/martijndwars/webpush/HttpEce.java
@@ -12,6 +12,7 @@
import java.nio.ByteBuffer;
import java.security.*;
import java.util.Arrays;
+import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@@ -437,7 +438,7 @@ private static byte[] intToBytes(int number) {
*/
private static byte[] log(String info, byte[] array) {
if ("1".equals(System.getenv("ECE_KEYLOG"))) {
- System.out.println(info + " [" + array.length + "]: " + Base64Encoder.encodeUrlWithoutPadding(array));
+ System.out.println(info + " [" + array.length + "]: " + Base64.getUrlEncoder().withoutPadding().encodeToString(array));
}
return array;
diff --git a/src/main/java/nl/martijndwars/webpush/HttpRequest.java b/src/main/java/nl/martijndwars/webpush/HttpRequest.java
new file mode 100644
index 0000000..b871a8a
--- /dev/null
+++ b/src/main/java/nl/martijndwars/webpush/HttpRequest.java
@@ -0,0 +1,31 @@
+package nl.martijndwars.webpush;
+
+import java.util.Map;
+
+public class HttpRequest {
+
+ private final String url;
+
+ private final Map headers;
+
+ private final byte[] body;
+
+ public HttpRequest(String url, Map headers, byte[] body) {
+ this.url = url;
+ this.headers = headers;
+ this.body = body;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+
+ public byte[] getBody() {
+ return body;
+ }
+
+}
diff --git a/src/main/java/nl/martijndwars/webpush/Notification.java b/src/main/java/nl/martijndwars/webpush/Notification.java
index 8a67e7b..6fdc493 100644
--- a/src/main/java/nl/martijndwars/webpush/Notification.java
+++ b/src/main/java/nl/martijndwars/webpush/Notification.java
@@ -8,6 +8,7 @@
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
+import java.util.Base64;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -32,40 +33,74 @@ public class Notification {
*/
private final byte[] payload;
+ /**
+ * Push Message Urgency
+ *
+ * @see Push Message Urgency
+ *
+ */
+ private Urgency urgency;
+
+ /**
+ * Push Message Topic
+ *
+ * @see Replacing Push Messages
+ *
+ */
+ private String topic;
+
/**
* Time in seconds that the push message is retained by the push service
*/
private final int ttl;
+ private static final int ONE_DAY_DURATION_IN_SECONDS = 86400;
+ private static int DEFAULT_TTL = 28 * ONE_DAY_DURATION_IN_SECONDS;
- public Notification(String endpoint, ECPublicKey userPublicKey, byte[] userAuth, byte[] payload, int ttl) {
+ public Notification(String endpoint, ECPublicKey userPublicKey, byte[] userAuth, byte[] payload, int ttl, Urgency urgency, String topic) {
this.endpoint = endpoint;
this.userPublicKey = userPublicKey;
this.userAuth = userAuth;
this.payload = payload;
this.ttl = ttl;
+ this.urgency = urgency;
+ this.topic = topic;
}
public Notification(String endpoint, PublicKey userPublicKey, byte[] userAuth, byte[] payload, int ttl) {
- this(endpoint, (ECPublicKey) userPublicKey, userAuth, payload, ttl);
+ this(endpoint, (ECPublicKey) userPublicKey, userAuth, payload, ttl, null, null);
+ }
+
+ public Notification(String endpoint, String userPublicKey, String userAuth, byte[] payload, int ttl) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+ this(endpoint, Utils.loadPublicKey(userPublicKey), Base64.getUrlDecoder().decode(userAuth), payload, ttl);
}
public Notification(String endpoint, PublicKey userPublicKey, byte[] userAuth, byte[] payload) {
- this(endpoint, userPublicKey, userAuth, payload, 2419200);
+ this(endpoint, userPublicKey, userAuth, payload, DEFAULT_TTL);
}
public Notification(String endpoint, String userPublicKey, String userAuth, byte[] payload) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
- this(endpoint, Utils.loadPublicKey(userPublicKey), Base64Encoder.decode(userAuth), payload);
+ this(endpoint, Utils.loadPublicKey(userPublicKey), Base64.getUrlDecoder().decode(userAuth), payload);
}
public Notification(String endpoint, String userPublicKey, String userAuth, String payload) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
- this(endpoint, Utils.loadPublicKey(userPublicKey), Base64Encoder.decode(userAuth), payload.getBytes(UTF_8));
+ this(endpoint, Utils.loadPublicKey(userPublicKey), Base64.getUrlDecoder().decode(userAuth), payload.getBytes(UTF_8));
}
+ public Notification(String endpoint, String userPublicKey, String userAuth, String payload, Urgency urgency) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+ this(endpoint, Utils.loadPublicKey(userPublicKey), Base64.getUrlDecoder().decode(userAuth), payload.getBytes(UTF_8));
+ this.urgency = urgency;
+ }
+
public Notification(Subscription subscription, String payload) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
this(subscription.endpoint, subscription.keys.p256dh, subscription.keys.auth, payload);
}
+ public Notification(Subscription subscription, String payload, Urgency urgency) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+ this(subscription.endpoint, subscription.keys.p256dh, subscription.keys.auth, payload);
+ this.urgency = urgency;
+ }
+
public String getEndpoint() {
return endpoint;
}
@@ -86,6 +121,14 @@ public boolean hasPayload() {
return getPayload().length > 0;
}
+ public boolean hasUrgency() {
+ return urgency != null;
+ }
+
+ public boolean hasTopic() {
+ return topic != null;
+ }
+
/**
* Detect if the notification is for a GCM-based subscription
*
@@ -103,9 +146,94 @@ public int getTTL() {
return ttl;
}
+ public Urgency getUrgency() {
+ return urgency;
+ }
+
+ public String getTopic() {
+ return topic;
+ }
+
public String getOrigin() throws MalformedURLException {
URL url = new URL(getEndpoint());
return url.getProtocol() + "://" + url.getHost();
}
+
+ public static NotificationBuilder builder() {
+ return new Notification.NotificationBuilder();
+ }
+
+ public static class NotificationBuilder {
+ private String endpoint = null;
+ private ECPublicKey userPublicKey = null;
+ private byte[] userAuth = null;
+ private byte[] payload = null;
+ private int ttl = DEFAULT_TTL;
+ private Urgency urgency = null;
+ private String topic = null;
+
+ private NotificationBuilder() {
+ }
+
+ public Notification build() {
+ return new Notification(endpoint, userPublicKey, userAuth, payload, ttl, urgency, topic);
+ }
+
+ public NotificationBuilder endpoint(String endpoint) {
+ this.endpoint = endpoint;
+ return this;
+ }
+
+ public NotificationBuilder userPublicKey(PublicKey publicKey) {
+ this.userPublicKey = (ECPublicKey) publicKey;
+ return this;
+ }
+
+ public NotificationBuilder userPublicKey(String publicKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+ this.userPublicKey = (ECPublicKey) Utils.loadPublicKey(publicKey);
+ return this;
+ }
+
+ public NotificationBuilder userPublicKey(byte[] publicKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+ this.userPublicKey = (ECPublicKey) Utils.loadPublicKey(publicKey);
+ return this;
+ }
+
+ public NotificationBuilder userAuth(String userAuth) {
+ this.userAuth = Base64.getUrlDecoder().decode(userAuth);
+ return this;
+ }
+
+ public NotificationBuilder userAuth(byte[] userAuth) {
+ this.userAuth = userAuth;
+ return this;
+ }
+
+ public NotificationBuilder payload(byte[] payload) {
+ this.payload = payload;
+ return this;
+ }
+
+ public NotificationBuilder payload(String payload) {
+ this.payload = payload.getBytes(UTF_8);
+ return this;
+ }
+
+ public NotificationBuilder ttl(int ttl) {
+ this.ttl = ttl;
+ return this;
+ }
+
+ public NotificationBuilder urgency(Urgency urgency) {
+ this.urgency = urgency;
+ return this;
+ }
+
+ public NotificationBuilder topic(String topic) {
+ this.topic = topic;
+ return this;
+ }
+ }
+
}
diff --git a/src/main/java/nl/martijndwars/webpush/PushAsyncService.java b/src/main/java/nl/martijndwars/webpush/PushAsyncService.java
new file mode 100644
index 0000000..870a7c9
--- /dev/null
+++ b/src/main/java/nl/martijndwars/webpush/PushAsyncService.java
@@ -0,0 +1,80 @@
+package nl.martijndwars.webpush;
+
+import org.asynchttpclient.AsyncHttpClient;
+import org.asynchttpclient.BoundRequestBuilder;
+import org.asynchttpclient.Response;
+import org.jose4j.lang.JoseException;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.concurrent.CompletableFuture;
+
+import static org.asynchttpclient.Dsl.asyncHttpClient;
+
+public class PushAsyncService extends AbstractPushService {
+
+ private final AsyncHttpClient httpClient = asyncHttpClient();
+
+ public PushAsyncService() {
+ }
+
+ public PushAsyncService(String gcmApiKey) {
+ super(gcmApiKey);
+ }
+
+ public PushAsyncService(KeyPair keyPair) {
+ super(keyPair);
+ }
+
+ public PushAsyncService(KeyPair keyPair, String subject) {
+ super(keyPair, subject);
+ }
+
+ public PushAsyncService(String publicKey, String privateKey) throws GeneralSecurityException {
+ super(publicKey, privateKey);
+ }
+
+ public PushAsyncService(String publicKey, String privateKey, String subject) throws GeneralSecurityException {
+ super(publicKey, privateKey, subject);
+ }
+
+ /**
+ * Send a notification asynchronously.
+ *
+ * @param notification
+ * @param encoding
+ * @return
+ * @throws GeneralSecurityException
+ * @throws IOException
+ * @throws JoseException
+ */
+ public CompletableFuture send(Notification notification, Encoding encoding) throws GeneralSecurityException, IOException, JoseException {
+ BoundRequestBuilder httpPost = preparePost(notification, encoding);
+ return httpPost.execute().toCompletableFuture();
+ }
+
+ public CompletableFuture send(Notification notification) throws GeneralSecurityException, IOException, JoseException {
+ return send(notification, Encoding.AES128GCM);
+ }
+
+ /**
+ * Prepare a POST request for AHC.
+ *
+ * @param notification
+ * @param encoding
+ * @return
+ * @throws GeneralSecurityException
+ * @throws IOException
+ * @throws JoseException
+ */
+ public BoundRequestBuilder preparePost(Notification notification, Encoding encoding) throws GeneralSecurityException, IOException, JoseException {
+ HttpRequest request = prepareRequest(notification, encoding);
+ BoundRequestBuilder httpPost = httpClient.preparePost(request.getUrl());
+ request.getHeaders().forEach(httpPost::addHeader);
+ if (request.getBody() != null) {
+ httpPost.setBody(request.getBody());
+ }
+ return httpPost;
+ }
+}
diff --git a/src/main/java/nl/martijndwars/webpush/PushService.java b/src/main/java/nl/martijndwars/webpush/PushService.java
index 60b2d97..e15647b 100644
--- a/src/main/java/nl/martijndwars/webpush/PushService.java
+++ b/src/main/java/nl/martijndwars/webpush/PushService.java
@@ -23,98 +23,29 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
-public class PushService {
- private static final SecureRandom SECURE_RANDOM = new SecureRandom();
- public static final String SERVER_KEY_ID = "server-key-id";
- public static final String SERVER_KEY_CURVE = "P-256";
-
- /**
- * The Google Cloud Messaging API key (for pre-VAPID in Chrome)
- */
- private String gcmApiKey;
-
- /**
- * Subject used in the JWT payload (for VAPID)
- */
- private String subject;
-
- /**
- * The public key (for VAPID)
- */
- private PublicKey publicKey;
-
- /**
- * The private key (for VAPID)
- */
- private PrivateKey privateKey;
+public class PushService extends AbstractPushService {
public PushService() {
}
public PushService(String gcmApiKey) {
- this.gcmApiKey = gcmApiKey;
+ super(gcmApiKey);
}
- public PushService(KeyPair keyPair, String subject) {
- this.publicKey = keyPair.getPublic();
- this.privateKey = keyPair.getPrivate();
- this.subject = subject;
+ public PushService(KeyPair keyPair) {
+ super(keyPair);
}
- public PushService(String publicKey, String privateKey, String subject) throws GeneralSecurityException {
- this.publicKey = Utils.loadPublicKey(publicKey);
- this.privateKey = Utils.loadPrivateKey(privateKey);
- this.subject = subject;
+ public PushService(KeyPair keyPair, String subject) {
+ super(keyPair, subject);
}
- /**
- * Encrypt the payload.
- *
- * Encryption uses Elliptic curve Diffie-Hellman (ECDH) cryptography over the prime256v1 curve.
- *
- * @param payload Payload to encrypt.
- * @param userPublicKey The user agent's public key (keys.p256dh).
- * @param userAuth The user agent's authentication secret (keys.auth).
- * @param encoding
- * @return An Encrypted object containing the public key, salt, and ciphertext.
- * @throws GeneralSecurityException
- */
- public static Encrypted encrypt(byte[] payload, ECPublicKey userPublicKey, byte[] userAuth, Encoding encoding) throws GeneralSecurityException {
- KeyPair localKeyPair = generateLocalKeyPair();
-
- Map keys = new HashMap<>();
- keys.put(SERVER_KEY_ID, localKeyPair);
-
- Map labels = new HashMap<>();
- labels.put(SERVER_KEY_ID, SERVER_KEY_CURVE);
-
- byte[] salt = new byte[16];
- SECURE_RANDOM.nextBytes(salt);
-
- HttpEce httpEce = new HttpEce(keys, labels);
- byte[] ciphertext = httpEce.encrypt(payload, salt, null, SERVER_KEY_ID, userPublicKey, userAuth, encoding);
-
- return new Encrypted.Builder()
- .withSalt(salt)
- .withPublicKey(localKeyPair.getPublic())
- .withCiphertext(ciphertext)
- .build();
+ public PushService(String publicKey, String privateKey) throws GeneralSecurityException {
+ super(publicKey, privateKey);
}
- /**
- * Generate the local (ephemeral) keys.
- *
- * @return
- * @throws NoSuchAlgorithmException
- * @throws NoSuchProviderException
- * @throws InvalidAlgorithmParameterException
- */
- private static KeyPair generateLocalKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
- ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec("prime256v1");
- KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDH", "BC");
- keyPairGenerator.initialize(parameterSpec);
-
- return keyPairGenerator.generateKeyPair();
+ public PushService(String publicKey, String privateKey, String subject) throws GeneralSecurityException {
+ super(publicKey, privateKey, subject);
}
/**
@@ -134,7 +65,7 @@ public HttpResponse send(Notification notification, Encoding encoding) throws Ge
}
public HttpResponse send(Notification notification) throws GeneralSecurityException, IOException, JoseException, ExecutionException, InterruptedException {
- return send(notification, Encoding.AESGCM);
+ return send(notification, Encoding.AES128GCM);
}
/**
@@ -146,7 +77,10 @@ public HttpResponse send(Notification notification) throws GeneralSecurityExcept
* @throws GeneralSecurityException
* @throws IOException
* @throws JoseException
+ *
+ * @deprecated Use {@link PushAsyncService#send(Notification, Encoding)} instead.
*/
+ @Deprecated
public Future sendAsync(Notification notification, Encoding encoding) throws GeneralSecurityException, IOException, JoseException {
HttpPost httpPost = preparePost(notification, encoding);
@@ -156,6 +90,10 @@ public Future sendAsync(Notification notification, Encoding encodi
return closeableHttpAsyncClient.execute(httpPost, new ClosableCallback(closeableHttpAsyncClient));
}
+ /**
+ * @deprecated Use {@link PushAsyncService#send(Notification)} instead.
+ */
+ @Deprecated
public Future sendAsync(Notification notification) throws GeneralSecurityException, IOException, JoseException {
return sendAsync(notification, Encoding.AES128GCM);
}
@@ -171,193 +109,12 @@ public Future sendAsync(Notification notification) throws GeneralS
* @throws JoseException
*/
public HttpPost preparePost(Notification notification, Encoding encoding) throws GeneralSecurityException, IOException, JoseException {
- if (privateKey != null && publicKey != null) {
- if (!Utils.verifyKeyPair(privateKey, publicKey)) {
- throw new IllegalStateException("Public key and private key do not match.");
- }
+ HttpRequest request = prepareRequest(notification, encoding);
+ HttpPost httpPost = new HttpPost(request.getUrl());
+ request.getHeaders().forEach(httpPost::addHeader);
+ if (request.getBody() != null) {
+ httpPost.setEntity(new ByteArrayEntity(request.getBody()));
}
-
- Encrypted encrypted = encrypt(
- notification.getPayload(),
- notification.getUserPublicKey(),
- notification.getUserAuth(),
- encoding
- );
-
- byte[] dh = Utils.encode((ECPublicKey) encrypted.getPublicKey());
- byte[] salt = encrypted.getSalt();
-
- HttpPost httpPost = new HttpPost(notification.getEndpoint());
- httpPost.addHeader("TTL", String.valueOf(notification.getTTL()));
-
- Map headers = new HashMap<>();
-
- if (notification.hasPayload()) {
- headers.put("Content-Type", "application/octet-stream");
-
- if (encoding == Encoding.AES128GCM) {
- headers.put("Content-Encoding", "aes128gcm");
- } else if (encoding == Encoding.AESGCM) {
- headers.put("Content-Encoding", "aesgcm");
- headers.put("Encryption", "salt=" + Base64Encoder.encodeUrlWithoutPadding(salt));
- headers.put("Crypto-Key", "dh=" + Base64Encoder.encodeUrl(dh));
- }
-
- httpPost.setEntity(new ByteArrayEntity(encrypted.getCiphertext()));
- }
-
- if (notification.isGcm()) {
- if (gcmApiKey == null) {
- throw new IllegalStateException("An GCM API key is needed to send a push notification to a GCM endpoint.");
- }
-
- headers.put("Authorization", "key=" + gcmApiKey);
- } else if (vapidEnabled()) {
- if (encoding == Encoding.AES128GCM) {
- if (notification.getEndpoint().startsWith("https://fcm.googleapis.com")) {
- httpPost.setURI(URI.create(notification.getEndpoint().replace("fcm/send", "wp")));
- }
- }
-
- JwtClaims claims = new JwtClaims();
- claims.setAudience(notification.getOrigin());
- claims.setExpirationTimeMinutesInTheFuture(12 * 60);
- claims.setSubject(subject);
-
- JsonWebSignature jws = new JsonWebSignature();
- jws.setHeader("typ", "JWT");
- jws.setHeader("alg", "ES256");
- jws.setPayload(claims.toJson());
- jws.setKey(privateKey);
- jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
-
- byte[] pk = Utils.encode((ECPublicKey) publicKey);
-
- if (encoding == Encoding.AES128GCM) {
- headers.put("Authorization", "vapid t=" + jws.getCompactSerialization() + ", k=" + Base64Encoder.encodeUrlWithoutPadding(pk));
- } else if (encoding == Encoding.AESGCM) {
- headers.put("Authorization", "WebPush " + jws.getCompactSerialization());
- }
-
- if (headers.containsKey("Crypto-Key")) {
- headers.put("Crypto-Key", headers.get("Crypto-Key") + ";p256ecdsa=" + Base64Encoder.encodeUrlWithoutPadding(pk));
- } else {
- headers.put("Crypto-Key", "p256ecdsa=" + Base64Encoder.encodeUrl(pk));
- }
- } else if (notification.isFcm() && gcmApiKey != null) {
- headers.put("Authorization", "key=" + gcmApiKey);
- }
-
- for (Map.Entry entry : headers.entrySet()) {
- httpPost.addHeader(new BasicHeader(entry.getKey(), entry.getValue()));
- }
-
return httpPost;
}
-
- /**
- * Set the Google Cloud Messaging (GCM) API key
- *
- * @param gcmApiKey
- * @return
- */
- public PushService setGcmApiKey(String gcmApiKey) {
- this.gcmApiKey = gcmApiKey;
-
- return this;
- }
-
- /**
- * Set the JWT subject (for VAPID)
- *
- * @param subject
- * @return
- */
- public PushService setSubject(String subject) {
- this.subject = subject;
-
- return this;
- }
-
- /**
- * Set the public and private key (for VAPID).
- *
- * @param keyPair
- * @return
- */
- public PushService setKeyPair(KeyPair keyPair) {
- setPublicKey(keyPair.getPublic());
- setPrivateKey(keyPair.getPrivate());
-
- return this;
- }
-
- public PublicKey getPublicKey() {
- return publicKey;
- }
-
- /**
- * Set the public key using a base64url-encoded string.
- *
- * @param publicKey
- * @return
- */
- public PushService setPublicKey(String publicKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
- setPublicKey(Utils.loadPublicKey(publicKey));
-
- return this;
- }
-
- public PrivateKey getPrivateKey() {
- return privateKey;
- }
-
- public KeyPair getKeyPair() {
- return new KeyPair(publicKey, privateKey);
- }
-
- /**
- * Set the public key (for VAPID)
- *
- * @param publicKey
- * @return
- */
- public PushService setPublicKey(PublicKey publicKey) {
- this.publicKey = publicKey;
-
- return this;
- }
-
- /**
- * Set the public key using a base64url-encoded string.
- *
- * @param privateKey
- * @return
- */
- public PushService setPrivateKey(String privateKey) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
- setPrivateKey(Utils.loadPrivateKey(privateKey));
-
- return this;
- }
-
- /**
- * Set the private key (for VAPID)
- *
- * @param privateKey
- * @return
- */
- public PushService setPrivateKey(PrivateKey privateKey) {
- this.privateKey = privateKey;
-
- return this;
- }
-
- /**
- * Check if VAPID is enabled
- *
- * @return
- */
- protected boolean vapidEnabled() {
- return publicKey != null && privateKey != null;
- }
}
diff --git a/src/main/java/nl/martijndwars/webpush/Urgency.java b/src/main/java/nl/martijndwars/webpush/Urgency.java
new file mode 100644
index 0000000..39d5b35
--- /dev/null
+++ b/src/main/java/nl/martijndwars/webpush/Urgency.java
@@ -0,0 +1,24 @@
+package nl.martijndwars.webpush;
+
+
+/**
+ * Web Push Message Urgency header field values
+ *
+ * @see Push Message Urgency
+ */
+public enum Urgency {
+ VERY_LOW("very-low"),
+ LOW("low"),
+ NORMAL("normal"),
+ HIGH("high");
+
+ private final String headerValue;
+
+ Urgency(String urgency) {
+ this.headerValue = urgency;
+ }
+
+ public String getHeaderValue() {
+ return headerValue;
+ }
+}
diff --git a/src/main/java/nl/martijndwars/webpush/Utils.java b/src/main/java/nl/martijndwars/webpush/Utils.java
index f5553d6..d135f38 100644
--- a/src/main/java/nl/martijndwars/webpush/Utils.java
+++ b/src/main/java/nl/martijndwars/webpush/Utils.java
@@ -15,6 +15,7 @@
import java.nio.ByteBuffer;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
+import java.util.Base64;
import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
@@ -45,7 +46,16 @@ public static byte[] encode(ECPrivateKey privateKey) {
* @param encodedPublicKey
*/
public static PublicKey loadPublicKey(String encodedPublicKey) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
- byte[] decodedPublicKey = Base64Encoder.decode(encodedPublicKey);
+ byte[] decodedPublicKey = Base64.getUrlDecoder().decode(encodedPublicKey);
+ return loadPublicKey(decodedPublicKey);
+ }
+
+ /**
+ * Load the public key from a byte array.
+ *
+ * @param decodedPublicKey
+ */
+ public static PublicKey loadPublicKey(byte[] decodedPublicKey) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM, PROVIDER_NAME);
ECParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec(CURVE);
ECCurve curve = parameterSpec.getCurve();
@@ -65,7 +75,20 @@ public static PublicKey loadPublicKey(String encodedPublicKey) throws NoSuchProv
* @throws InvalidKeySpecException
*/
public static PrivateKey loadPrivateKey(String encodedPrivateKey) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
- byte[] decodedPrivateKey = Base64Encoder.decode(encodedPrivateKey);
+ byte[] decodedPrivateKey = Base64.getUrlDecoder().decode(encodedPrivateKey);
+ return loadPrivateKey(decodedPrivateKey);
+ }
+
+ /**
+ * Load the private key from a byte array
+ *
+ * @param decodedPrivateKey
+ * @return
+ * @throws NoSuchProviderException
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeySpecException
+ */
+ public static PrivateKey loadPrivateKey(byte[] decodedPrivateKey) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
BigInteger s = BigIntegers.fromUnsignedByteArray(decodedPrivateKey);
ECParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec(CURVE);
ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(s, parameterSpec);
diff --git a/src/main/java/nl/martijndwars/webpush/cli/handlers/GenerateKeyHandler.java b/src/main/java/nl/martijndwars/webpush/cli/handlers/GenerateKeyHandler.java
index 68bfd99..1747adf 100644
--- a/src/main/java/nl/martijndwars/webpush/cli/handlers/GenerateKeyHandler.java
+++ b/src/main/java/nl/martijndwars/webpush/cli/handlers/GenerateKeyHandler.java
@@ -1,6 +1,5 @@
package nl.martijndwars.webpush.cli.handlers;
-import nl.martijndwars.webpush.Base64Encoder;
import nl.martijndwars.webpush.Utils;
import nl.martijndwars.webpush.cli.commands.GenerateKeyCommand;
import org.bouncycastle.jce.ECNamedCurveTable;
@@ -15,6 +14,7 @@
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.security.*;
+import java.util.Base64;
import static nl.martijndwars.webpush.Utils.ALGORITHM;
import static nl.martijndwars.webpush.Utils.CURVE;
@@ -42,10 +42,10 @@ public void run() throws InvalidAlgorithmParameterException, NoSuchAlgorithmExce
}
System.out.println("PublicKey:");
- System.out.println(Base64Encoder.encodeUrl(encodedPublicKey));
+ System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString(encodedPublicKey));
System.out.println("PrivateKey:");
- System.out.println(Base64Encoder.encodeUrl(encodedPrivateKey));
+ System.out.println(Base64.getUrlEncoder().withoutPadding().encodeToString(encodedPrivateKey));
}
/**
diff --git a/src/test/java/nl/martijndwars/webpush/Base64EncoderTest.java b/src/test/java/nl/martijndwars/webpush/Base64EncoderTest.java
deleted file mode 100644
index 962e405..0000000
--- a/src/test/java/nl/martijndwars/webpush/Base64EncoderTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package nl.martijndwars.webpush;
-
-import org.junit.jupiter.api.Test;
-
-import static com.google.common.io.BaseEncoding.base64;
-import static com.google.common.io.BaseEncoding.base64Url;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static nl.martijndwars.webpush.Base64Encoder.*;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-class Base64EncoderTest {
-
- @Test
- void decodeTest() {
- // first compare with previous guava implementation, make sure non-breaking changes
- assertEquals(new String(base64().decode("")), new String(decode("")));
- assertEquals(new String(base64().decode("dw")), new String(decode("dw")));
- assertEquals(new String(base64().decode("dw==")), new String(decode("dw==")));
- assertEquals(new String(base64().decode("d2U")), new String(decode("d2U")));
- assertEquals(new String(base64().decode("d2Vi")), new String(decode("d2Vi")));
- assertEquals(new String(base64().decode("d2ViLQ")), new String(decode("d2ViLQ")));
- assertEquals(new String(base64().decode("d2ViLQ==")), new String(decode("d2ViLQ==")));
- assertEquals(new String(base64().decode("d2ViLXA")), new String(decode("d2ViLXA")));
- assertEquals(new String(base64().decode("d2ViLXA=")), new String(decode("d2ViLXA=")));
- assertEquals(new String(base64().decode("d2ViLXB1")), new String(decode("d2ViLXB1")));
- assertEquals(new String(base64().decode("d2ViLXB1cw")), new String(decode("d2ViLXB1cw")));
- assertEquals(new String(base64().decode("d2ViLXB1cw==")), new String(decode("d2ViLXB1cw==")));
- assertEquals(new String(base64().decode("d2ViLXB1c2g")), new String(decode("d2ViLXB1c2g")));
- assertEquals(new String(base64().decode("d2ViLXB1c2g=")), new String(decode("d2ViLXB1c2g=")));
- assertEquals(new String(base64().decode("d2ViLXB1c2g/")), new String(decode("d2ViLXB1c2g/")));
- assertEquals(new String(base64Url().decode("d2ViLXB1c2g_")), new String(decode("d2ViLXB1c2g_")));
-
- assertEquals("", new String(decode("")));
- assertEquals("w", new String(decode("dw")));
- assertEquals("w", new String(decode("dw==")));
- assertEquals("we", new String(decode("d2U")));
- assertEquals("web", new String(decode("d2Vi")));
- assertEquals("web-", new String(decode("d2ViLQ")));
- assertEquals("web-", new String(decode("d2ViLQ==")));
- assertEquals("web-p", new String(decode("d2ViLXA")));
- assertEquals("web-p", new String(decode("d2ViLXA=")));
- assertEquals("web-pu", new String(decode("d2ViLXB1")));
- assertEquals("web-pus", new String(decode("d2ViLXB1cw")));
- assertEquals("web-pus", new String(decode("d2ViLXB1cw==")));
- assertEquals("web-push", new String(decode("d2ViLXB1c2g")));
- assertEquals("web-push", new String(decode("d2ViLXB1c2g=")));
- assertEquals("web-push?", new String(decode("d2ViLXB1c2g/")));
- assertEquals("web-push?", new String(decode("d2ViLXB1c2g_")));
- }
-
- @Test
- void encodeWithoutPaddingTest() {
- // first verify non breaking changes after removing guava as compile dependency
- assertEquals(base64().omitPadding().encode("".getBytes()), encodeWithoutPadding("".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("w".getBytes()), encodeWithoutPadding("w".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("we".getBytes()), encodeWithoutPadding("we".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("web".getBytes()), encodeWithoutPadding("web".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("web-".getBytes()), encodeWithoutPadding("web-".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("web-p".getBytes()), encodeWithoutPadding("web-p".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("web-pu".getBytes()), encodeWithoutPadding("web-pu".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("web-pus".getBytes()), encodeWithoutPadding("web-pus".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("web-push".getBytes()), encodeWithoutPadding("web-push".getBytes(UTF_8)));
- assertEquals(base64().omitPadding().encode("web-push?".getBytes()), encodeWithoutPadding("web-push?".getBytes(UTF_8)));
-
- assertEquals("", encodeWithoutPadding("".getBytes(UTF_8)));
- assertEquals("dw", encodeWithoutPadding("w".getBytes(UTF_8)));
- assertEquals("d2U", encodeWithoutPadding("we".getBytes(UTF_8)));
- assertEquals("d2Vi", encodeWithoutPadding("web".getBytes(UTF_8)));
- assertEquals("d2ViLQ", encodeWithoutPadding("web-".getBytes(UTF_8)));
- assertEquals("d2ViLXA", encodeWithoutPadding("web-p".getBytes(UTF_8)));
- assertEquals("d2ViLXB1", encodeWithoutPadding("web-pu".getBytes(UTF_8)));
- assertEquals("d2ViLXB1cw", encodeWithoutPadding("web-pus".getBytes(UTF_8)));
- assertEquals("d2ViLXB1c2g", encodeWithoutPadding("web-push".getBytes(UTF_8)));
- assertEquals("d2ViLXB1c2g/", encodeWithoutPadding("web-push?".getBytes(UTF_8)));
- }
-
- @Test
- void encodeUrlTest() {
- // first verify non breaking changes after removing guava as compile dependency
- assertEquals(base64Url().encode("".getBytes()), encodeUrl("".getBytes(UTF_8)));
- assertEquals(base64Url().encode("w".getBytes()), encodeUrl("w".getBytes(UTF_8)));
- assertEquals(base64Url().encode("we".getBytes()), encodeUrl("we".getBytes(UTF_8)));
- assertEquals(base64Url().encode("web".getBytes()), encodeUrl("web".getBytes(UTF_8)));
- assertEquals(base64Url().encode("web-".getBytes()), encodeUrl("web-".getBytes(UTF_8)));
- assertEquals(base64Url().encode("web-p".getBytes()), encodeUrl("web-p".getBytes(UTF_8)));
- assertEquals(base64Url().encode("web-pu".getBytes()), encodeUrl("web-pu".getBytes(UTF_8)));
- assertEquals(base64Url().encode("web-pus".getBytes()), encodeUrl("web-pus".getBytes(UTF_8)));
- assertEquals(base64Url().encode("web-push".getBytes()), encodeUrl("web-push".getBytes(UTF_8)));
- assertEquals(base64Url().encode("web-push?".getBytes()), encodeUrl("web-push?".getBytes(UTF_8)));
-
- assertEquals("", encodeUrl("".getBytes(UTF_8)));
- assertEquals("dw==", encodeUrl("w".getBytes(UTF_8)));
- assertEquals("d2U=", encodeUrl("we".getBytes(UTF_8)));
- assertEquals("d2Vi", encodeUrl("web".getBytes(UTF_8)));
- assertEquals("d2ViLQ==", encodeUrl("web-".getBytes(UTF_8)));
- assertEquals("d2ViLXA=", encodeUrl("web-p".getBytes(UTF_8)));
- assertEquals("d2ViLXB1", encodeUrl("web-pu".getBytes(UTF_8)));
- assertEquals("d2ViLXB1cw==", encodeUrl("web-pus".getBytes(UTF_8)));
- assertEquals("d2ViLXB1c2g=", encodeUrl("web-push".getBytes(UTF_8)));
- assertEquals("d2ViLXB1c2g_", encodeUrl("web-push?".getBytes(UTF_8)));
- }
-
- @Test
- void encodeUrlWithoutPaddingTest() {
- // first verify non breaking changes after removing guava as compile dependency
- assertEquals(base64Url().omitPadding().encode("".getBytes()), encodeUrlWithoutPadding("".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("w".getBytes()), encodeUrlWithoutPadding("w".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("we".getBytes()), encodeUrlWithoutPadding("we".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("web".getBytes()), encodeUrlWithoutPadding("web".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("web-".getBytes()), encodeUrlWithoutPadding("web-".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("web-p".getBytes()), encodeUrlWithoutPadding("web-p".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("web-pu".getBytes()), encodeUrlWithoutPadding("web-pu".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("web-pus".getBytes()), encodeUrlWithoutPadding("web-pus".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("web-push".getBytes()), encodeUrlWithoutPadding("web-push".getBytes(UTF_8)));
- assertEquals(base64Url().omitPadding().encode("web-push?".getBytes()), encodeUrlWithoutPadding("web-push?".getBytes(UTF_8)));
-
- assertEquals("", encodeUrlWithoutPadding("".getBytes(UTF_8)));
- assertEquals("dw", encodeUrlWithoutPadding("w".getBytes(UTF_8)));
- assertEquals("d2U", encodeUrlWithoutPadding("we".getBytes(UTF_8)));
- assertEquals("d2Vi", encodeUrlWithoutPadding("web".getBytes(UTF_8)));
- assertEquals("d2ViLQ", encodeUrlWithoutPadding("web-".getBytes(UTF_8)));
- assertEquals("d2ViLXA", encodeUrlWithoutPadding("web-p".getBytes(UTF_8)));
- assertEquals("d2ViLXB1", encodeUrlWithoutPadding("web-pu".getBytes(UTF_8)));
- assertEquals("d2ViLXB1cw", encodeUrlWithoutPadding("web-pus".getBytes(UTF_8)));
- assertEquals("d2ViLXB1c2g", encodeUrlWithoutPadding("web-push".getBytes(UTF_8)));
- assertEquals("d2ViLXB1c2g_", encodeUrlWithoutPadding("web-push?".getBytes(UTF_8)));
- }
-}
\ No newline at end of file
diff --git a/src/test/java/nl/martijndwars/webpush/HttpEceTest.java b/src/test/java/nl/martijndwars/webpush/HttpEceTest.java
index fadc408..c3f6884 100644
--- a/src/test/java/nl/martijndwars/webpush/HttpEceTest.java
+++ b/src/test/java/nl/martijndwars/webpush/HttpEceTest.java
@@ -7,9 +7,9 @@
import org.junit.jupiter.api.Test;
import java.security.*;
+import java.util.Base64;
import java.util.HashMap;
-import static nl.martijndwars.webpush.Base64Encoder.decode;
import static nl.martijndwars.webpush.Encoding.AES128GCM;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -19,6 +19,10 @@ public static void addSecurityProvider() {
Security.addProvider(new BouncyCastleProvider());
}
+ private byte[] decode(String s) {
+ return Base64.getUrlDecoder().decode(s);
+ }
+
@Test
public void testZeroSaltAndKey() throws GeneralSecurityException {
HttpEce httpEce = new HttpEce();
diff --git a/src/test/java/nl/martijndwars/webpush/NotificationTest.java b/src/test/java/nl/martijndwars/webpush/NotificationTest.java
new file mode 100644
index 0000000..a9d1c6c
--- /dev/null
+++ b/src/test/java/nl/martijndwars/webpush/NotificationTest.java
@@ -0,0 +1,43 @@
+package nl.martijndwars.webpush;
+
+import java.security.GeneralSecurityException;
+import java.security.Security;
+import java.time.Duration;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class NotificationTest {
+
+ private static final String endpoint = "https://the-url.co.uk";
+ private static final String publicKey = "BGu3hOwCLOBfdMReXf7-SD2x5tKs_vPapOneyngBOnu6PgNYdgLPKFAodfBnG60MqkXC0McPFehN2Kyuh6TKm14=";
+ private static int oneDayDurationInSeconds = 86400;
+
+ @BeforeAll
+ public static void addSecurityProvider() {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ @Test
+ public void testNotificationBuilder() throws GeneralSecurityException {
+ Notification notification = Notification.builder()
+ .endpoint(endpoint)
+ .userPublicKey(publicKey)
+ .payload(new byte[16])
+ .ttl((int) Duration.ofDays(15).getSeconds())
+ .build();
+ assertEquals(endpoint, notification.getEndpoint());
+ assertEquals(15 * oneDayDurationInSeconds, notification.getTTL());
+ }
+
+ @Test
+ public void testDefaultTtl() throws GeneralSecurityException {
+ Notification notification = Notification.builder()
+ .userPublicKey(publicKey)
+ .payload(new byte[16])
+ .build();
+ assertEquals(28 * oneDayDurationInSeconds, notification.getTTL());
+ }
+}
diff --git a/src/test/java/nl/martijndwars/webpush/selenium/SeleniumTests.java b/src/test/java/nl/martijndwars/webpush/selenium/SeleniumTests.java
index 00522bb..1054486 100644
--- a/src/test/java/nl/martijndwars/webpush/selenium/SeleniumTests.java
+++ b/src/test/java/nl/martijndwars/webpush/selenium/SeleniumTests.java
@@ -1,6 +1,5 @@
package nl.martijndwars.webpush.selenium;
-import nl.martijndwars.webpush.Base64Encoder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.DynamicTest;
@@ -8,6 +7,7 @@
import java.io.IOException;
import java.security.Security;
+import java.util.Base64;
import java.util.stream.Stream;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
@@ -58,21 +58,21 @@ public Stream dynamicTests() throws IOException {
* @return
*/
protected Stream getConfigurations() {
- String PUBLIC_KEY_NO_PADDING = Base64Encoder.encodeWithoutPadding(
- Base64Encoder.decode(PUBLIC_KEY)
+ String PUBLIC_KEY_NO_PADDING = Base64.getUrlEncoder().withoutPadding().encodeToString(
+ Base64.getUrlDecoder().decode(PUBLIC_KEY)
);
return Stream.of(
new Configuration("chrome", "stable", null, GCM_SENDER_ID),
new Configuration("chrome", "beta", null, GCM_SENDER_ID),
- new Configuration("chrome", "unstable", null, GCM_SENDER_ID),
+ //new Configuration("chrome", "unstable", null, GCM_SENDER_ID), See #90
new Configuration("firefox", "stable", null, GCM_SENDER_ID),
new Configuration("firefox", "beta", null, GCM_SENDER_ID),
new Configuration("chrome", "stable", PUBLIC_KEY_NO_PADDING, null),
new Configuration("chrome", "beta", PUBLIC_KEY_NO_PADDING, null),
- new Configuration("chrome", "unstable", PUBLIC_KEY_NO_PADDING, null),
+ //new Configuration("chrome", "unstable", PUBLIC_KEY_NO_PADDING, null), See #90
new Configuration("firefox", "stable", PUBLIC_KEY_NO_PADDING, null),
new Configuration("firefox", "beta", PUBLIC_KEY_NO_PADDING, null)
diff --git a/src/test/java/nl/martijndwars/webpush/selenium/TestingService.java b/src/test/java/nl/martijndwars/webpush/selenium/TestingService.java
index 8d0f7ed..a89f9a9 100644
--- a/src/test/java/nl/martijndwars/webpush/selenium/TestingService.java
+++ b/src/test/java/nl/martijndwars/webpush/selenium/TestingService.java
@@ -33,7 +33,7 @@ public TestingService(String baseUrl) {
public int startTestSuite() throws IOException {
String startTestSuite = request(baseUrl + "start-test-suite/");
- JsonElement root = new JsonParser().parse(startTestSuite);
+ JsonElement root = JsonParser.parseString(startTestSuite);
return root
.getAsJsonObject()
@@ -129,7 +129,7 @@ protected String request(String uri, HttpEntity entity) throws IOException {
String json = EntityUtils.toString(httpResponse.getEntity());
if (httpResponse.getStatusLine().getStatusCode() != 200) {
- JsonElement root = new JsonParser().parse(json);
+ JsonElement root = JsonParser.parseString(json);
JsonObject error = root.getAsJsonObject().get("error").getAsJsonObject();
String errorId = error.get("id").getAsString();
@@ -150,7 +150,7 @@ protected String request(String uri, HttpEntity entity) throws IOException {
* @param response
*/
protected JsonObject getData(String response) {
- JsonElement root = new JsonParser().parse(response);
+ JsonElement root = JsonParser.parseString(response);
return root
.getAsJsonObject()