Repository: relayrides/pushy
Pull request #556 (2017-11-24 15:53:17)

Generate authorization headers as ASCII strings from tokens
This is a very mild optimization to allow authentication tokens to produce their own authorization header values as ASCII strings. At the expense of a few extra bytes of storage per token (about one per open connection), we can save one string concatenation _per notification_ and make the most of Netty's `AsciiString` capabilities.


Patch:
diff --git a/pushy/src/main/java/com/turo/pushy/apns/TokenAuthenticationApnsClientHandler.java b/pushy/src/main/java/com/turo/pushy/apns/TokenAuthenticationApnsClientHandler.java
index 21a9a17c..8db82695 100644
--- a/pushy/src/main/java/com/turo/pushy/apns/TokenAuthenticationApnsClientHandler.java
+++ b/pushy/src/main/java/com/turo/pushy/apns/TokenAuthenticationApnsClientHandler.java
@@ -97,7 +97,7 @@ class TokenAuthenticationApnsClientHandler extends ApnsClientHandler {
             }
         }
 
-        headers.add(APNS_AUTHORIZATION_HEADER, "bearer " + this.authenticationToken.toString());
+        headers.add(APNS_AUTHORIZATION_HEADER, this.authenticationToken.getAuthorizationHeader());
 
         return headers;
     }
diff --git a/pushy/src/main/java/com/turo/pushy/apns/auth/AuthenticationToken.java b/pushy/src/main/java/com/turo/pushy/apns/auth/AuthenticationToken.java
index 48c72027..06fe7b99 100644
--- a/pushy/src/main/java/com/turo/pushy/apns/auth/AuthenticationToken.java
+++ b/pushy/src/main/java/com/turo/pushy/apns/auth/AuthenticationToken.java
@@ -26,6 +26,7 @@ import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.annotations.SerializedName;
 import com.turo.pushy.apns.util.DateAsTimeSinceEpochTypeAdapter;
+import io.netty.util.AsciiString;
 import org.apache.commons.codec.binary.Base64;
 
 import java.nio.charset.StandardCharsets;
@@ -111,7 +112,8 @@ public class AuthenticationToken {
     private final AuthenticationTokenClaims claims;
     private final byte[] signatureBytes;
 
-    private final String base64EncodedToken;
+    private transient final String base64EncodedToken;
+    private transient final AsciiString authorizationHeader;
 
     /**
      * Constructs a new authentication token using the given signing key (and associated metadata) issued at the given
@@ -149,6 +151,7 @@ public class AuthenticationToken {
         payloadBuilder.append(Base64.encodeBase64URLSafeString(this.signatureBytes));
 
         this.base64EncodedToken = payloadBuilder.toString();
+        this.authorizationHeader = new AsciiString("bearer " + payloadBuilder.toString());
     }
 
     /**
@@ -161,8 +164,9 @@ public class AuthenticationToken {
         Objects.requireNonNull(base64EncodedToken, "Encoded token must not be null.");
 
         this.base64EncodedToken = base64EncodedToken;
+        this.authorizationHeader = new AsciiString("bearer " + base64EncodedToken);
 
-        final String[] jwtSegments = this.base64EncodedToken.split("\\.");
+        final String[] jwtSegments = base64EncodedToken.split("\\.");
 
         if (jwtSegments.length != 3) {
             throw new IllegalArgumentException();
@@ -241,6 +245,15 @@ public class AuthenticationToken {
         return signature.verify(this.signatureBytes);
     }
 
+    /**
+     * Returns a complete APNs authorization header value (i.e. "bearer [token]") for this authentication token.
+     *
+     * @return a complete APNs authorization header value for this authentication token
+     */
+    public AsciiString getAuthorizationHeader() {
+        return authorizationHeader;
+    }
+
     /**
      * Returns a Base64-encoded JWT representation of this authentication token.
      *


Test patch:
diff --git a/pushy/src/test/java/com/turo/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandlerTest.java b/pushy/src/test/java/com/turo/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandlerTest.java
index f79e43af..422b63a5 100644
--- a/pushy/src/test/java/com/turo/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandlerTest.java
+++ b/pushy/src/test/java/com/turo/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandlerTest.java
@@ -75,7 +75,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
     protected void addAcceptableCredentialsToHeaders(final Http2Headers headers) throws Exception {
         final AuthenticationToken authenticationToken = new AuthenticationToken(this.signingKey, new Date());
 
-        headers.set(APNS_AUTHORIZATION_HEADER, "bearer " + authenticationToken.toString());
+        headers.set(APNS_AUTHORIZATION_HEADER, authenticationToken.getAuthorizationHeader());
     }
 
     @Test
@@ -141,7 +141,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
 
         final AuthenticationToken authenticationToken = new AuthenticationToken(signingKey, new Date());
 
-        this.headers.set(APNS_AUTHORIZATION_HEADER, "bearer " + authenticationToken.toString());
+        this.headers.set(APNS_AUTHORIZATION_HEADER, authenticationToken.getAuthorizationHeader());
 
         this.testWithExpectedRejection("Push notifications with authentication tokens with unknown keys should be rejected.",
                 handler,
@@ -157,7 +157,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
         final ApnsSigningKey unverifiedKey = new ApnsSigningKey(KEY_ID, TEAM_ID, (ECPrivateKey) keyPair.getPrivate());
         final AuthenticationToken unverifiedToken = new AuthenticationToken(unverifiedKey, new Date());
 
-        this.headers.set(APNS_AUTHORIZATION_HEADER, "bearer " + unverifiedToken.toString());
+        this.headers.set(APNS_AUTHORIZATION_HEADER, unverifiedToken.getAuthorizationHeader());
 
         this.testWithExpectedRejection("Push notifications with an authentication token that can't be verified by the registered public key should be rejected.",
                 this.getHandler(DEVICE_TOKENS_BY_TOPIC, Collections.<String, Date>emptyMap()),
@@ -170,7 +170,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
     public void testHandleNotificationWithWithExpiredAuthenticationToken() throws Exception {
         final AuthenticationToken expiredToken = new AuthenticationToken(this.signingKey, new Date(0));
 
-        this.headers.set(APNS_AUTHORIZATION_HEADER, "bearer " + expiredToken.toString());
+        this.headers.set(APNS_AUTHORIZATION_HEADER, expiredToken.getAuthorizationHeader());
 
         this.testWithExpectedRejection("Push notifications with an expired authentication token should be rejected.",
                 this.getHandler(DEVICE_TOKENS_BY_TOPIC, Collections.<String, Date>emptyMap()),
@@ -192,7 +192,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
 
         final AuthenticationToken authenticationToken = new AuthenticationToken(signingKey, new Date());
 
-        this.headers.set(APNS_AUTHORIZATION_HEADER, "bearer " + authenticationToken.toString());
+        this.headers.set(APNS_AUTHORIZATION_HEADER, authenticationToken.getAuthorizationHeader());
 
         this.testWithExpectedRejection("Push notifications for topics not associated with a valid verification key should be rejected.",
                 handler,