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

io.github.yawenok.apns.http2.impl.NettyApnsHttp2Connection Maven / Gradle / Ivy

package io.github.yawenok.apns.http2.impl;

import io.github.yawenok.apns.http2.concurrent.FutureCallback;
import io.github.yawenok.apns.http2.config.auth.JWTAuthConfig;
import io.github.yawenok.apns.http2.impl.initializer.Http2ClientInitializer;
import io.github.yawenok.apns.http2.impl.model.ResponseFuture;
import io.github.yawenok.apns.http2.impl.model.ResponseFutureCallback;
import io.github.yawenok.apns.http2.utils.P12Utils;
import io.github.yawenok.apns.http2.config.AuthConfig;
import io.github.yawenok.apns.http2.config.auth.TLSAuthConfig;
import io.github.yawenok.apns.http2.ApnsConstant;
import io.github.yawenok.apns.http2.config.ProxyConfig;
import io.github.yawenok.apns.http2.ApnsHttp2Connection;
import io.github.yawenok.apns.http2.enums.auth.AuthMode;
import io.github.yawenok.apns.http2.Notification;
import io.github.yawenok.apns.http2.NotificationResponse;
import io.github.yawenok.apns.http2.exceptions.AuthenticationException;
import io.github.yawenok.apns.http2.exceptions.ConnectionException;
import io.github.yawenok.apns.http2.utils.JWTUtils;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.*;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;

import javax.net.ssl.SSLException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;

/**
 * Creates An APNs connection based on the HTTP/2 network protocol.
 *
 * @author yawen
 *
 * @see Communicating with APNs
 */
public class NettyApnsHttp2Connection implements ApnsHttp2Connection {
    private String host;
    private int port;

    private String topic;
    private String jwt;

    private long scopeExpiration = 30 * 60 * 1000;
    private long scopeBegin = 0;

    private AuthConfig authConfig;

    private Http2ClientInitializer http2ClientInitializer;

    private Channel channel;
    private EventLoopGroup workerGroup;

    @Override
    public void init(final String host, final int port, final AuthConfig authConfig, final ProxyConfig proxyConfig) throws AuthenticationException, ConnectionException {
        this.host = host;
        this.port = port;
        this.authConfig = authConfig;

        try {
            if (authConfig.getAuthMode() == AuthMode.TLS) {
                TLSAuthConfig tlsAuthConfig = (TLSAuthConfig) authConfig;
                KeyStore keyStore = P12Utils.loadKeyStoreFromPCKS12File(tlsAuthConfig.getLicenceFile(), tlsAuthConfig.getPassword());
                List identities = P12Utils.getIdentitiesFromKeyStore(keyStore);

                this.topic = identities.get(0);
                this.http2ClientInitializer = new Http2ClientInitializer(this.createSslContext(tlsAuthConfig.isJdkDefault(), keyStore, tlsAuthConfig.getPassword()), proxyConfig);
            } else if (authConfig.getAuthMode() == AuthMode.JWT) {
                JWTAuthConfig jwtAuthConfig = (JWTAuthConfig) authConfig;

                this.jwt = JWTUtils.createJWT(jwtAuthConfig.getPrivateKeyFile(), jwtAuthConfig.getKeyId(), jwtAuthConfig.getTeamId());
                this.scopeBegin = System.currentTimeMillis();
                this.http2ClientInitializer = new Http2ClientInitializer(this.createSslContext(jwtAuthConfig.isJdkDefault(), jwtAuthConfig.getCaFile()), proxyConfig);
            } else {
                throw new AuthenticationException("AuthConfig can't get AuthMode!");
            }
        } catch (Exception e) {
            throw new AuthenticationException("Init authentication error!", e);
        }

        try {
            this.doConnect(host, port);
        } catch (Exception e) {
            throw new ConnectionException("Connect error!", e);
        }
    }

    @Override
    public void destroy() {
        this.disConnect();
    }

    @Override
    public Future sendNotification(final Notification notification) throws ConnectionException {
        if (!this.isActive()) {
            this.reActive();
        }

        final String uuid = UUID.randomUUID().toString();
        final FullHttpRequest request = buildRequest(notification, uuid);
        final ResponseFuture responseFuture = new ResponseFuture();

        http2ClientInitializer.putResponseFuture(uuid, responseFuture);
        try {
            channel.writeAndFlush(request);
        } catch (Exception e) {
            http2ClientInitializer.cleanApns(uuid);
            throw new ConnectionException("Channel is not active!");
        }
        return responseFuture;
    }

    @Override
    public void sendNotification(final Notification notification, final FutureCallback callback) throws ConnectionException {
        if (!this.isActive()) {
            this.reActive();
        }

        final String uuid = UUID.randomUUID().toString();
        final FullHttpRequest request = buildRequest(notification, uuid);
        final ResponseFutureCallback responseFutureCallback = new ResponseFutureCallback(notification, callback);

        http2ClientInitializer.putResponseFutureCallback(uuid, responseFutureCallback);
        try {
            channel.writeAndFlush(request);
        } catch (Exception e) {
            http2ClientInitializer.cleanApns(uuid);
            throw new ConnectionException("Channel is not active!");
        }
    }

    @Override
    public boolean isActive() {
        if (authConfig.getAuthMode() == AuthMode.JWT) {
            // JWT only can use 1 hour for APNs
            if (System.currentTimeMillis() - scopeBegin > scopeExpiration) {
                return false;
            }
        }

        if (channel != null) {
            return channel.isActive();
        }
        return false;
    }

    @Override
    public synchronized void reActive() throws ConnectionException {
        if (authConfig.getAuthMode() == AuthMode.JWT) {
            try {
                JWTAuthConfig jwtAuthConfig = (JWTAuthConfig) authConfig;

                jwt = JWTUtils.createJWT(jwtAuthConfig.getPrivateKeyFile(), jwtAuthConfig.getKeyId(), jwtAuthConfig.getTeamId());
                scopeBegin = System.currentTimeMillis();
            } catch (Exception e) {
                throw new ConnectionException("Create a JWT error!", e);
            }
        }

        this.disConnect();
        try {
            this.doConnect(host, port);
        } catch (Exception e) {
            throw new ConnectionException("Re connect error!", e);
        }
    }

    private FullHttpRequest buildRequest(final Notification notification, final String uuid) {
        // The request for Http1.1 will automatically converted to Http2.
        final String uri = "https://" + host + "/3/device/" + notification.getToken().replace(" ", "");
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uri, Unpooled.copiedBuffer(notification.getPayload().toString().getBytes()));
        final HttpHeaders headers = request.headers();

        headers.add(ApnsConstant.APNS_NOTIFICATION_ID, uuid);
        if (authConfig.getAuthMode() == AuthMode.JWT) {
            headers.add(ApnsConstant.APNS_AUTHORIZATION_HEADER, "bearer " + jwt);
        }
        if (notification.getTopic() != null) {
            headers.add(ApnsConstant.APNS_NOTIFICATION_TOPIC, notification.getTopic());
        } else if (topic != null) {
            headers.add(ApnsConstant.APNS_NOTIFICATION_TOPIC, topic);
        }
        if (notification.getExpiration() != null) {
            headers.addInt(ApnsConstant.APNS_NOTIFICATION_EXPIRATION, notification.getExpiration());
        }
        if (notification.getPriority() != null) {
            headers.addInt(ApnsConstant.APNS_NOTIFICATION_PRIORITY, notification.getPriority());
        }

        return request;
    }


    private void doConnect(final String host, final int port) throws SSLException {
        this.workerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(this.workerGroup);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
        bootstrap.option(ChannelOption.TCP_NODELAY, true);
        bootstrap.remoteAddress(host, port);
        bootstrap.handler(this.http2ClientInitializer);

        this.channel = bootstrap.connect().syncUninterruptibly().channel();
        this.http2ClientInitializer.awaitSettings(30, TimeUnit.SECONDS);
    }

    private void disConnect() {
        if (workerGroup != null) {
            workerGroup.shutdownGracefully();
            workerGroup = null;
        }

        if (channel != null) {
            channel.closeFuture().syncUninterruptibly();
            channel = null;
        }
    }

    private SslContext createSslContext(final boolean jdkDefault, final File caFile) throws FileNotFoundException, SSLException {
        SslContextBuilder sslContextBuilder = this.createSslContextBuilder(jdkDefault);
        return sslContextBuilder.trustManager(new FileInputStream(caFile)).build();
    }

    private SslContext createSslContext(final boolean jdkDefault, final KeyStore keyStore, final String password) throws SSLException {
        SslContextBuilder sslContextBuilder = this.createSslContextBuilder(jdkDefault);
        X509Certificate x509Certificate;
        PrivateKey privateKey;
        try {
            final KeyStore.PrivateKeyEntry privateKeyEntry = P12Utils.getFirstPrivateKeyEntryFromKeyStore(keyStore, password);
            final Certificate certificate = privateKeyEntry.getCertificate();
            if (!(certificate instanceof X509Certificate)) {
                throw new KeyStoreException("Found a certificate in the provided PKCS#12 file, but it was not an X.509 certificate.");
            }

            x509Certificate = (X509Certificate) certificate;
            privateKey = privateKeyEntry.getPrivateKey();
        } catch (KeyStoreException | IOException e) {
            throw new SSLException(e);
        }

        return sslContextBuilder.keyManager(privateKey, x509Certificate).build();
    }

    private SslContextBuilder createSslContextBuilder(final boolean jdkDefault) {
        SslProvider provider;
        if (jdkDefault) {
            provider = SslProvider.JDK;
        } else {
            provider = OpenSsl.isAvailable() && OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK;
        }

        return SslContextBuilder.forClient()
                .sslProvider(provider)
                .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .applicationProtocolConfig(new ApplicationProtocolConfig(
                        ApplicationProtocolConfig.Protocol.ALPN,
                        ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
                        ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
                        ApplicationProtocolNames.HTTP_2,
                        ApplicationProtocolNames.HTTP_1_1));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy