All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.relayrides.pushy.apns.MockApnsServer Maven / Gradle / Ivy

There is a newer version: 0.9.3
Show newest version
package com.relayrides.pushy.apns;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.interfaces.ECPublicKey;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.concurrent.DefaultPromise;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.GlobalEventExecutor;
import io.netty.util.concurrent.SucceededFuture;

/**
 * 

A mock APNs server emulates the behavior of a real APNs server (but doesn't actually deliver notifications to * their destinations). Mock servers are primarily useful for integration tests and benchmarks; most users will * not need to interact with mock servers.

* *

Mock servers maintain a registry of tokens for a variety of topics. When first created, no tokens are registered * with a mock server, and all attempts to send notifications will fail until at least one token is registered via the * {@link com.relayrides.pushy.apns.MockApnsServer#registerDeviceTokenForTopic(String, String, Date)} method.

* * @author Jon Chambers * * @since 0.8 */ public class MockApnsServer { private final ServerBootstrap bootstrap; private final boolean shouldShutDownEventLoopGroup; private final Map> tokenExpirationsByTopic = new HashMap<>(); private final Map signaturesByKeyId = new HashMap<>(); private final Map teamIdsByKeyId = new HashMap<>(); private final Map> topicsByTeamId = new HashMap<>(); private ChannelGroup allChannels; private boolean emulateInternalErrors = false; protected MockApnsServer(final SslContext sslContext, final EventLoopGroup eventLoopGroup) { this.bootstrap = new ServerBootstrap(); if (eventLoopGroup != null) { this.bootstrap.group(eventLoopGroup); this.shouldShutDownEventLoopGroup = false; } else { this.bootstrap.group(new NioEventLoopGroup(1)); this.shouldShutDownEventLoopGroup = true; } this.bootstrap.channel(SocketChannelClassUtil.getServerSocketChannelClass(this.bootstrap.config().group())); this.bootstrap.childHandler(new ChannelInitializer() { @Override protected void initChannel(final SocketChannel channel) throws Exception { final SslHandler sslHandler = sslContext.newHandler(channel.alloc()); channel.pipeline().addLast(sslHandler); channel.pipeline().addLast(new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) { @Override protected void configurePipeline(final ChannelHandlerContext context, final String protocol) throws Exception { if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { context.pipeline().addLast(new MockApnsServerHandler.MockApnsServerHandlerBuilder() .apnsServer(MockApnsServer.this) .initialSettings(new Http2Settings().maxConcurrentStreams(8)) .build()); MockApnsServer.this.allChannels.add(context.channel()); } else { throw new IllegalStateException("Unexpected protocol: " + protocol); } } }); } }); } /** * Starts this mock server and listens for traffic on the given port. * * @param port the port to which this server should bind * * @return a {@code Future} that will succeed when the server has bound to the given port and is ready to accept * traffic */ public Future start(final int port) { final ChannelFuture channelFuture = this.bootstrap.bind(port); this.allChannels = new DefaultChannelGroup(channelFuture.channel().eventLoop(), true); this.allChannels.add(channelFuture.channel()); return channelFuture; } /** * Registers a public key for verifying authentication tokens for the given topics. Clears any keys and topics * previously associated with the given team. * * @param publicKey a public key to be used to verify authentication tokens * @param teamId an identifier for the team to which the given public key belongs * @param keyId an identifier for the given public key * @param topics the topics belonging to the given team for which the given public key can be used to verify * authentication tokens * * @throws NoSuchAlgorithmException if the required signing algorithm is not available * @throws InvalidKeyException if the given key is invalid for any reason * * @since 0.9 */ public void registerPublicKey(final ECPublicKey publicKey, final String teamId, final String keyId, final Collection topics) throws NoSuchAlgorithmException, InvalidKeyException { this.registerPublicKey(publicKey, teamId, keyId, topics.toArray(new String[0])); } /** * Registers a public key for verifying authentication tokens for the given topics. Clears any keys and topics * previously associated with the given team. * * @param publicKey a public key to be used to verify authentication tokens * @param teamId an identifier for the team to which the given public key belongs * @param keyId an identifier for the given public key * @param topics the topics belonging to the given team for which the given public key can be used to verify * authentication tokens * * @throws NoSuchAlgorithmException if the required signing algorithm is not available * @throws InvalidKeyException if the given key is invalid for any reason * * @since 0.9 */ public void registerPublicKey(final ECPublicKey publicKey, final String teamId, final String keyId, final String... topics) throws NoSuchAlgorithmException, InvalidKeyException { // First, clear out any old keys/topics { final Set keyIdsToRemove = new HashSet<>(); for (final Map.Entry entry : this.teamIdsByKeyId.entrySet()) { if (entry.getValue().equals(teamId)) { keyIdsToRemove.add(entry.getKey()); } } for (final String keyIdToRemove : keyIdsToRemove) { this.teamIdsByKeyId.remove(keyIdToRemove); this.signaturesByKeyId.remove(keyIdToRemove); } this.topicsByTeamId.remove(teamId); } final Signature signature = Signature.getInstance("SHA256withECDSA"); signature.initVerify(publicKey); this.signaturesByKeyId.put(keyId, signature); this.teamIdsByKeyId.put(keyId, teamId); final Set topicSet = new HashSet<>(); for (final String topic : topics) { topicSet.add(topic); } this.topicsByTeamId.put(teamId, topicSet); } protected Signature getSignatureForKeyId(final String keyId) { return this.signaturesByKeyId.get(keyId); } protected String getTeamIdForKeyId(final String keyId) { return this.teamIdsByKeyId.get(keyId); } protected Set getTopicsForTeamId(final String teamId) { return this.topicsByTeamId.get(teamId); } /** * Unregisters all teams, topics, and public keys from this server. */ public void clearPublicKeys() { this.signaturesByKeyId.clear(); this.teamIdsByKeyId.clear(); this.topicsByTeamId.clear(); } /** * Registers a new token for a specific topic. Registered tokens may have an expiration date; attempts to send * notifications to tokens with expiration dates in the past will fail. * * @param topic the topic for which to register the given token * @param token the token to register * @param expiration the time at which the token expires (or expired); may be {@code null}, in which case the token * never expires */ public void registerDeviceTokenForTopic(final String topic, final String token, final Date expiration) { Objects.requireNonNull(topic); Objects.requireNonNull(token); if (!this.tokenExpirationsByTopic.containsKey(topic)) { this.tokenExpirationsByTopic.put(topic, new HashMap()); } this.tokenExpirationsByTopic.get(topic).put(token, expiration); } /** * Unregisters all tokens from this server. */ public void clearTokens() { this.tokenExpirationsByTopic.clear(); } protected boolean isTokenRegisteredForTopic(final String token, final String topic) { final Map tokensWithinTopic = this.tokenExpirationsByTopic.get(topic); return tokensWithinTopic != null && tokensWithinTopic.containsKey(token); } protected Date getExpirationTimestampForTokenInTopic(final String token, final String topic) { final Map tokensWithinTopic = this.tokenExpirationsByTopic.get(topic); return tokensWithinTopic != null ? tokensWithinTopic.get(token) : null; } protected void setEmulateInternalErrors(final boolean emulateInternalErrors) { this.emulateInternalErrors = emulateInternalErrors; } protected boolean shouldEmulateInternalErrors() { return this.emulateInternalErrors; } /** *

Shuts down this server and releases the port to which this server was bound. If a {@code null} event loop * group was provided at construction time, the server will also shut down its internally-managed event loop * group.

* *

If a non-null {@code EventLoopGroup} was provided at construction time, mock servers may be reconnected and * reused after they have been shut down. If no event loop group was provided at construction time, mock servers may * not be restarted after they have been shut down via this method.

* * @return a {@code Future} that will succeed once the server has finished unbinding from its port and, if the * server was managing its own event loop group, its event loop group has shut down */ @SuppressWarnings({ "rawtypes", "unchecked" }) public Future shutdown() { final Future channelCloseFuture = (this.allChannels != null) ? this.allChannels.close() : new SucceededFuture(GlobalEventExecutor.INSTANCE, null); final Future disconnectFuture; if (this.shouldShutDownEventLoopGroup) { // Wait for the channel to close before we try to shut down the event loop group channelCloseFuture.addListener(new GenericFutureListener>() { @Override public void operationComplete(final Future future) throws Exception { MockApnsServer.this.bootstrap.config().group().shutdownGracefully(); } }); // Since the termination future for the event loop group is a Future instead of a Future, // we'll need to create our own promise and then notify it when the termination future completes. disconnectFuture = new DefaultPromise<>(GlobalEventExecutor.INSTANCE); this.bootstrap.config().group().terminationFuture().addListener(new GenericFutureListener() { @Override public void operationComplete(final Future future) throws Exception { assert disconnectFuture instanceof DefaultPromise; ((DefaultPromise) disconnectFuture).trySuccess(null); } }); } else { // We're done once we've closed all the channels, so we can return the closure future directly. disconnectFuture = channelCloseFuture; } return disconnectFuture; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy