Skip to content

Commit

Permalink
Add support for the apns-unique-id header in development environments
Browse files Browse the repository at this point in the history
  • Loading branch information
hectorespert authored Dec 28, 2023
1 parent d47352c commit 3302582
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
private static final AsciiString APNS_PRIORITY_HEADER = new AsciiString("apns-priority");
private static final AsciiString APNS_COLLAPSE_ID_HEADER = new AsciiString("apns-collapse-id");
private static final AsciiString APNS_ID_HEADER = new AsciiString("apns-id");
private static final AsciiString APNS_UNIQUE_ID_HEADER = new AsciiString("apns-unique-id");
private static final AsciiString APNS_PUSH_TYPE_HEADER = new AsciiString("apns-push-type");

private static final IOException STREAMS_EXHAUSTED_EXCEPTION =
Expand Down Expand Up @@ -325,7 +326,8 @@ private void handleEndOfStream(final ChannelHandlerContext context, final Http2S

if (HttpResponseStatus.OK.equals(status)) {
responseFuture.complete(new SimplePushNotificationResponse<>(responseFuture.getPushNotification(),
true, getApnsIdFromHeaders(headers), status.code(), null, null));
true, getApnsIdFromHeaders(headers), getApnsUniqueIdFromHeaders(headers),
status.code(), null, null));
} else {
if (data != null) {
ErrorResponse errorResponse;
Expand All @@ -352,17 +354,26 @@ protected void handleErrorResponse(final ChannelHandlerContext context, final in
final HttpResponseStatus status = HttpResponseStatus.parseLine(headers.status());

responseFuture.complete(new SimplePushNotificationResponse<>(responseFuture.getPushNotification(),
HttpResponseStatus.OK.equals(status), getApnsIdFromHeaders(headers), status.code(),
HttpResponseStatus.OK.equals(status), getApnsIdFromHeaders(headers),
getApnsUniqueIdFromHeaders(headers), status.code(),
errorResponse.getReason(), errorResponse.getTimestamp()));
}

private static UUID getApnsIdFromHeaders(final Http2Headers headers) {
final CharSequence apnsIdSequence = headers.get(APNS_ID_HEADER);
return getUUIDFromHeaders(headers, APNS_ID_HEADER);
}

private static UUID getApnsUniqueIdFromHeaders(final Http2Headers headers) {
return getUUIDFromHeaders(headers, APNS_UNIQUE_ID_HEADER);
}

private static UUID getUUIDFromHeaders(final Http2Headers headers, final AsciiString header) {
final CharSequence apnsIdSequence = headers.get(header);

try {
return apnsIdSequence != null ? FastUUID.parseUUID(apnsIdSequence) : null;
} catch (final IllegalArgumentException e) {
log.error("Failed to parse `apns-id` header: {}", apnsIdSequence, e);
log.error("Failed to parse `{}` header: {}", header, apnsIdSequence, e);
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ public interface PushNotificationResponse<T extends ApnsPushNotification> {
*/
UUID getApnsId();


/**
* Returns a unique ID only available in the development environment.
* Useful to query push information in Push Notifications Console.
* @return UUID The apns unique id
*/
default Optional<UUID> getApnsUniqueId() {
return Optional.empty();
}

/**
* Returns the HTTP status code reported by the APNs server.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ class SimplePushNotificationResponse<T extends ApnsPushNotification> implements
private final T pushNotification;
private final boolean success;
private final UUID apnsId;
private final UUID apnsUniqueId;
private final int statusCode;
private final String rejectionReason;
private final Instant tokenExpirationTimestamp;

SimplePushNotificationResponse(final T pushNotification, final boolean success, final UUID apnsId, final int statusCode, final String rejectionReason, final Instant tokenExpirationTimestamp) {
SimplePushNotificationResponse(final T pushNotification, final boolean success, final UUID apnsId, final UUID apnsUniqueId, final int statusCode, final String rejectionReason, final Instant tokenExpirationTimestamp) {
this.pushNotification = pushNotification;
this.success = success;
this.apnsId = apnsId;
this.apnsUniqueId = apnsUniqueId;
this.statusCode = statusCode;
this.rejectionReason = rejectionReason;
this.tokenExpirationTimestamp = tokenExpirationTimestamp;
Expand All @@ -65,6 +67,11 @@ public UUID getApnsId() {
return this.apnsId;
}

@Override
public Optional<UUID> getApnsUniqueId() {
return Optional.ofNullable(apnsUniqueId);
}

@Override
public int getStatusCode() {
return this.statusCode;
Expand All @@ -86,6 +93,7 @@ public String toString() {
"pushNotification=" + pushNotification +
", success=" + success +
", apnsId=" + (apnsId != null ? FastUUID.toString(apnsId) : null) +
", apnsUniqueId=" + getApnsUniqueId().map(FastUUID::toString).orElse(null) +
", rejectionReason='" + rejectionReason + '\'' +
", tokenExpirationTimestamp=" + tokenExpirationTimestamp +
'}';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,19 @@ public class MockApnsServer extends BaseHttp2Server {
private final MockApnsServerListener listener;

private final int maxConcurrentStreams;
private final boolean generateApnsUniqueId;

MockApnsServer(final SslContext sslContext, final EventLoopGroup eventLoopGroup,
final PushNotificationHandlerFactory handlerFactory, final MockApnsServerListener listener,
final int maxConcurrentStreams) {
final int maxConcurrentStreams, boolean generateApnsUniqueId) {

super(sslContext, eventLoopGroup);

this.handlerFactory = handlerFactory;
this.listener = listener;

this.maxConcurrentStreams = maxConcurrentStreams;
this.generateApnsUniqueId = generateApnsUniqueId;
}

@Override
Expand All @@ -74,6 +76,7 @@ protected void addHandlersToPipeline(final SSLSession sslSession, final ChannelP
.pushNotificationHandler(pushNotificationHandler)
.initialSettings(Http2Settings.defaultSettings().maxConcurrentStreams(this.maxConcurrentStreams))
.listener(this.listener)
.generateApnsUniqueId(generateApnsUniqueId)
.build();

pipeline.addLast(serverHandler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class MockApnsServerBuilder extends BaseHttp2ServerBuilder<MockApnsServer

private PushNotificationHandlerFactory handlerFactory;
private MockApnsServerListener listener;
private boolean generateApnsUniqueId = false;

@Override
public MockApnsServerBuilder setServerCredentials(final File certificatePemFile, final File privateKeyPkcs8File, final String privateKeyPassword) {
Expand Down Expand Up @@ -133,6 +134,11 @@ public MockApnsServerBuilder setListener(final MockApnsServerListener listener)
return this;
}

public MockApnsServerBuilder generateApnsUniqueId(boolean generateApnsUniqueId) {
this.generateApnsUniqueId = generateApnsUniqueId;
return this;
}

@Override
public MockApnsServer build() throws SSLException {
return super.build();
Expand All @@ -144,6 +150,6 @@ protected MockApnsServer constructServer(final SslContext sslContext) {
throw new IllegalStateException("Must provide a push notification handler factory before building a mock server.");
}

return new MockApnsServer(sslContext, this.eventLoopGroup, this.handlerFactory, this.listener, this.maxConcurrentStreams);
return new MockApnsServer(sslContext, this.eventLoopGroup, this.handlerFactory, this.listener, this.maxConcurrentStreams, generateApnsUniqueId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

class MockApnsServerHandler extends Http2ConnectionHandler implements Http2FrameListener {
Expand All @@ -50,8 +51,12 @@ class MockApnsServerHandler extends Http2ConnectionHandler implements Http2Frame
private final Http2Connection.PropertyKey headersPropertyKey;
private final Http2Connection.PropertyKey payloadPropertyKey;

private final boolean generateApnsUniqueId;

private static final AsciiString APNS_ID_HEADER = new AsciiString("apns-id");

private static final AsciiString APNS_UNIQUE_ID_HEADER = new AsciiString("apns-unique-id");

private static final int MAX_CONTENT_LENGTH = 4096;

private static final Logger log = LoggerFactory.getLogger(MockApnsServerHandler.class);
Expand All @@ -60,6 +65,7 @@ public static class MockApnsServerHandlerBuilder extends AbstractHttp2Connection

private PushNotificationHandler pushNotificationHandler;
private MockApnsServerListener listener;
private boolean generateApnsUniqueId = false;

MockApnsServerHandlerBuilder pushNotificationHandler(final PushNotificationHandler pushNotificationHandler) {
this.pushNotificationHandler = pushNotificationHandler;
Expand All @@ -76,9 +82,14 @@ public MockApnsServerHandlerBuilder initialSettings(final Http2Settings initialS
return super.initialSettings(initialSettings);
}

public MockApnsServerHandlerBuilder generateApnsUniqueId(final boolean generateApnsUniqueId) {
this.generateApnsUniqueId = generateApnsUniqueId;
return this;
}

@Override
public MockApnsServerHandler build(final Http2ConnectionDecoder decoder, final Http2ConnectionEncoder encoder, final Http2Settings initialSettings) {
final MockApnsServerHandler handler = new MockApnsServerHandler(decoder, encoder, initialSettings, this.pushNotificationHandler, this.listener);
final MockApnsServerHandler handler = new MockApnsServerHandler(decoder, encoder, initialSettings, this.pushNotificationHandler, this.listener, generateApnsUniqueId);
this.frameListener(handler);
return handler;
}
Expand All @@ -92,10 +103,12 @@ public MockApnsServerHandler build() {
private static abstract class ApnsResponse {
private final int streamId;
private final UUID apnsId;
private final UUID apnsUniqueId;

private ApnsResponse(final int streamId, final UUID apnsId) {
private ApnsResponse(final int streamId, final UUID apnsId, UUID apnsUniqueId) {
this.streamId = streamId;
this.apnsId = apnsId;
this.apnsUniqueId = apnsUniqueId;
}

int getStreamId() {
Expand All @@ -105,20 +118,24 @@ int getStreamId() {
UUID getApnsId() {
return apnsId;
}

Optional<UUID> getApnsUniqueId() {
return Optional.ofNullable(apnsUniqueId);
}
}

private static class AcceptNotificationResponse extends ApnsResponse {
private AcceptNotificationResponse(final int streamId, final UUID apnsId) {
super(streamId, apnsId);
private AcceptNotificationResponse(final int streamId, final UUID apnsId, final UUID apnsUniqueId) {
super(streamId, apnsId, apnsUniqueId);
}
}

private static class RejectNotificationResponse extends ApnsResponse {
private final RejectionReason errorReason;
private final Instant timestamp;

RejectNotificationResponse(final int streamId, final UUID apnsId, final RejectionReason errorReason, final Instant timestamp) {
super(streamId, apnsId);
RejectNotificationResponse(final int streamId, final UUID apnsId, final UUID apnsUniqueId, final RejectionReason errorReason, final Instant timestamp) {
super(streamId, apnsId, apnsUniqueId);

this.errorReason = errorReason;
this.timestamp = timestamp;
Expand Down Expand Up @@ -159,9 +176,11 @@ public void handlePushNotificationRejected(final Http2Headers headers, final Byt
final Http2ConnectionEncoder encoder,
final Http2Settings initialSettings,
final PushNotificationHandler pushNotificationHandler,
final MockApnsServerListener listener) {
final MockApnsServerListener listener,
boolean generateApnsUniqueId) {

super(decoder, encoder, initialSettings);
this.generateApnsUniqueId = generateApnsUniqueId;

this.headersPropertyKey = this.connection().newKey();
this.payloadPropertyKey = this.connection().newKey();
Expand Down Expand Up @@ -267,19 +286,21 @@ private void handleEndOfStream(final ChannelHandlerContext context, final Http2S
apnsId = apnsIdFromHeaders;
}

final UUID apnsUniqueId = generateApnsUniqueId ? UUID.randomUUID() : null;

try {
this.pushNotificationHandler.handlePushNotification(headers, payload);

this.write(context, new AcceptNotificationResponse(stream.id(), apnsId), writePromise);
this.write(context, new AcceptNotificationResponse(stream.id(), apnsId, apnsUniqueId), writePromise);
this.listener.handlePushNotificationAccepted(headers, payload);
} catch (final RejectedNotificationException e) {
final Instant deviceTokenExpirationTimestamp = e instanceof UnregisteredDeviceTokenException ?
((UnregisteredDeviceTokenException) e).getDeviceTokenExpirationTimestamp() : null;

this.write(context, new RejectNotificationResponse(stream.id(), apnsId, e.getRejectionReason(), deviceTokenExpirationTimestamp), writePromise);
this.write(context, new RejectNotificationResponse(stream.id(), apnsId, apnsUniqueId, e.getRejectionReason(), deviceTokenExpirationTimestamp), writePromise);
this.listener.handlePushNotificationRejected(headers, payload, e.getRejectionReason(), deviceTokenExpirationTimestamp);
} catch (final Exception e) {
this.write(context, new RejectNotificationResponse(stream.id(), apnsId, RejectionReason.INTERNAL_SERVER_ERROR, null), writePromise);
this.write(context, new RejectNotificationResponse(stream.id(), apnsId, apnsUniqueId, RejectionReason.INTERNAL_SERVER_ERROR, null), writePromise);
this.listener.handlePushNotificationRejected(headers, payload, RejectionReason.INTERNAL_SERVER_ERROR, null);
} finally {
if (stream.getProperty(this.payloadPropertyKey) != null) {
Expand All @@ -299,6 +320,9 @@ public void write(final ChannelHandlerContext context, final Object message, fin
.status(HttpResponseStatus.OK.codeAsText())
.add(APNS_ID_HEADER, FastUUID.toString(acceptNotificationResponse.getApnsId()));

acceptNotificationResponse.getApnsUniqueId()
.ifPresent(apnsUniqueId -> headers.add(APNS_UNIQUE_ID_HEADER, FastUUID.toString(apnsUniqueId)));

this.encoder().writeHeaders(context, acceptNotificationResponse.getStreamId(), headers, 0, true, writePromise);

log.trace("Accepted push notification on stream {}", acceptNotificationResponse.getStreamId());
Expand All @@ -310,6 +334,9 @@ public void write(final ChannelHandlerContext context, final Object message, fin
.add(HttpHeaderNames.CONTENT_TYPE, "application/json")
.add(APNS_ID_HEADER, FastUUID.toString(rejectNotificationResponse.getApnsId()));

rejectNotificationResponse.getApnsUniqueId()
.ifPresent(apnsUniqueId -> headers.add(APNS_UNIQUE_ID_HEADER, FastUUID.toString(apnsUniqueId)));

final byte[] payloadBytes;
{
final Map<String, Object> errorPayload = new HashMap<>(2, 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@
import com.eatthepath.pushy.apns.auth.ApnsSigningKey;
import com.eatthepath.pushy.apns.auth.ApnsVerificationKey;
import com.eatthepath.pushy.apns.auth.KeyPairUtil;
import com.eatthepath.pushy.apns.server.MockApnsServer;
import com.eatthepath.pushy.apns.server.MockApnsServerBuilder;
import com.eatthepath.pushy.apns.server.MockApnsServerListener;
import com.eatthepath.pushy.apns.server.PushNotificationHandlerFactory;
import com.eatthepath.pushy.apns.server.*;
import com.eatthepath.pushy.apns.util.ApnsPayloadBuilder;
import com.eatthepath.pushy.apns.util.SimpleApnsPayloadBuilder;
import io.netty.channel.nio.NioEventLoopGroup;
Expand Down Expand Up @@ -144,13 +141,22 @@ protected MockApnsServer buildServer(final PushNotificationHandlerFactory handle
return this.buildServer(handlerFactory, null);
}

protected MockApnsServer buildServer(final ValidatingPushNotificationHandlerFactory handlerFactory, final boolean generateApnsUniqueId) throws SSLException {
return this.buildServer(handlerFactory, null, generateApnsUniqueId);
}

protected MockApnsServer buildServer(final PushNotificationHandlerFactory handlerFactory, final MockApnsServerListener listener) throws SSLException {
return this.buildServer(handlerFactory, listener, false);
}

protected MockApnsServer buildServer(final PushNotificationHandlerFactory handlerFactory, final MockApnsServerListener listener, final boolean generateApnsUniqueId) throws SSLException {
return new MockApnsServerBuilder()
.setServerCredentials(getClass().getResourceAsStream(SERVER_CERTIFICATES_FILENAME), getClass().getResourceAsStream(SERVER_KEY_FILENAME), null)
.setTrustedClientCertificateChain(getClass().getResourceAsStream(CA_CERTIFICATE_FILENAME))
.setEventLoopGroup(SERVER_EVENT_LOOP_GROUP)
.setHandlerFactory(handlerFactory)
.setListener(listener)
.generateApnsUniqueId(generateApnsUniqueId)
.build();
}

Expand Down
Loading

0 comments on commit 3302582

Please sign in to comment.