0

I am creating a client to communicate with APNs.

here is my requirement.

  • jdk 1.6
  • http/2
  • tls 1.3
  • ALPN

so I decided to make it using Netty.
I don't know if I set the header and data well.

Http2Client.java

public class Http2Client {
//  static final boolean SSL = System.getProperty("ssl") != null;
    static final boolean SSL = true;
    static final String HOST = "api.sandbox.push.apple.com";
    static final int PORT = 443;
    static final String PATH = "/3/device/00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0";
    
//  private static final AsciiTest APNS_PATH = new AsciiTest("/3/device/00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0");
    private static final AsciiTest APNS_EXPIRATION_HEADER = new AsciiTest("apns-expiration");
    private static final AsciiTest APNS_TOPIC_HEADER = new AsciiTest("apns-topic");
    private static final AsciiTest APNS_PRIORITY_HEADER = new AsciiTest("apns-priority");
    private static final AsciiTest APNS_AUTHORIZATION = new AsciiTest("authorization");
    private static final AsciiTest APNS_ID_HEADER = new AsciiTest("apns-id");
    private static final AsciiTest APNS_PUSH_TYPE_HEADER = new AsciiTest("apns-push-type");
    
    public static void main(String[] args) throws Exception {
        
        EventLoopGroup clientWorkerGroup = new NioEventLoopGroup();

        // Configure SSL.
        final SslContext sslCtx;
        if (SSL) {
            SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL
                    : SslProvider.JDK;
            sslCtx = SslContextBuilder.forClient()
                    .sslProvider(provider)
                    /*
                     * NOTE: the cipher filter may not include all ciphers required by the HTTP/2
                     * specification. Please refer to the HTTP/2 specification for cipher
                     * requirements.
                     */
                    .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
                    
                    .trustManager(InsecureTrustManagerFactory.INSTANCE)
                    .applicationProtocolConfig(new ApplicationProtocolConfig(
                            Protocol.ALPN,
                            // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK
                            // providers.
                            SelectorFailureBehavior.NO_ADVERTISE,
                            // ACCEPT is currently the only mode supported by both OpenSsl and JDK
                            // providers.
                            SelectedListenerFailureBehavior.ACCEPT,
                            ApplicationProtocolNames.HTTP_2,
                            ApplicationProtocolNames.HTTP_1_1))
                    .build();
        } else {
            sslCtx = null;
        }

        try {
            // Configure the client.
            Bootstrap b = new Bootstrap();
            b.group(clientWorkerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.remoteAddress(HOST, PORT);
            b.handler(new Http2ClientInit(sslCtx));

            // Start the client.
            final Channel channel = b.connect().syncUninterruptibly().channel();
            System.out.println("Connected to [" + HOST + ':' + PORT + ']');

            final Http2ResponseHandler streamFrameResponseHandler =
                    new Http2ResponseHandler();

            final Http2StreamChannelBootstrap streamChannelBootstrap = new Http2StreamChannelBootstrap(channel);
            final Http2StreamChannel streamChannel = streamChannelBootstrap.open().syncUninterruptibly().getNow();
            streamChannel.pipeline().addLast(streamFrameResponseHandler);

            // Send request (a HTTP/2 HEADERS frame - with ':method = POST' in this case)
            final Http2Headers headers = new DefaultHttp2Headers();
            headers.method(HttpMethod.POST.asciiName());
            headers.path(PATH);
            headers.scheme(HttpScheme.HTTPS.name());
            
            headers.add(APNS_TOPIC_HEADER, "com.example.MyApp");
            headers.add(APNS_AUTHORIZATION,
                    "bearer eyAia2lkIjogIjhZTDNHM1JSWDciIH0.eyAiaXNzIjogIkM4Nk5WOUpYM0QiLCAiaWF0IjogIjE0NTkxNDM1ODA2NTAiIH0.MEYCIQDzqyahmH1rz1s-LFNkylXEa2lZ_aOCX4daxxTZkVEGzwIhALvkClnx5m5eAT6Lxw7LZtEQcH6JENhJTMArwLf3sXwi");
            headers.add(APNS_ID_HEADER, "eabeae54-14a8-11e5-b60b-1697f925ec7b");
            headers.add(APNS_PUSH_TYPE_HEADER, "alert");
            headers.add(APNS_EXPIRATION_HEADER, "0");
            headers.add(APNS_PRIORITY_HEADER, "10");
            
            final Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers, true);
            streamChannel.writeAndFlush(headersFrame);
            System.out.println("Sent HTTP/2 POST request to " + PATH);


            // Wait for the responses (or for the latch to expire), then clean up the
            // connections
            if (!streamFrameResponseHandler.responseSuccessfullyCompleted()) {
                System.err.println("Did not get HTTP/2 response in expected time.");
            }

            System.out.println("Finished HTTP/2 request, will close the connection.");

            // Wait until the connection is closed.
            channel.close().syncUninterruptibly();
        } finally {
            clientWorkerGroup.shutdownGracefully();
        }
    }
}

Http2ResponseHandler.java

public final class Http2ResponseHandler extends SimpleChannelInboundHandler<Http2StreamFrame> {

    private final CountDownLatch latch = new CountDownLatch(1);

    public void channelActive(ChannelHandlerContext ctx) {
        

        String sendMessage = "{\"aps\":{\"alert\":\"hello\"}}";
        
        
        ByteBuf messageBuffer = Unpooled.buffer();
        messageBuffer.writeBytes(sendMessage.getBytes());

        StringBuilder builder = new StringBuilder();
        builder.append("request [");
        builder.append(sendMessage);
        builder.append("]");

        System.out.println(builder.toString());

        
        ctx.writeAndFlush(new DefaultHttp2DataFrame(messageBuffer, true));
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Http2StreamFrame msg) throws Exception {
        ByteBuf content = ctx.alloc().buffer();
        System.out.println(content);
        System.out.println("Received HTTP/2 'stream' frame : " + msg);
        
        // isEndStream() is not from a common interface, so we currently must check both
        if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) {
            ByteBuf data = ((DefaultHttp2DataFrame) msg).content().alloc().buffer();
            System.out.println(data.readCharSequence(256, Charset.forName("utf-8")).toString());
            latch.countDown();
        } else if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) {
            latch.countDown();
        }
//      String readMessage = ((ByteBuf) msg).toString(CharsetUtil.UTF_8);
//
//      StringBuilder builder = new StringBuilder();
//      builder.append("receive [");
//      builder.append(readMessage);
//      builder.append("]");
//
//      System.out.println(builder.toString());
    }

    public void channelReadComplete(ChannelHandlerContext ctx) {
//       ctx.flush();
        ctx.close();
    }

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
    /**
     * Waits for the latch to be decremented (i.e. for an end of stream message to be received), or for
     * the latch to expire after 5 seconds.
     * @return true if a successful HTTP/2 end of stream message was received.
     */
    public boolean responseSuccessfullyCompleted() {
        try {
            return latch.await(5, TimeUnit.SECONDS);
        } catch (InterruptedException ie) {
            System.err.println("Latch exception: " + ie.getMessage());
            return false;
        }
    }

}

console log

Connected to [api.sandbox.push.apple.com:443]
Sent HTTP/2 POST request to /3/device/00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0
PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)
Received HTTP/2 'stream' frame : DefaultHttp2HeadersFrame(stream=3, headers=DefaultHttp2Headers[:status: 403, apns-id: eabeae54-14a8-11e5-b60b-1697f925ec7b], endStream=false, padding=0)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)
Received HTTP/2 'stream' frame : DefaultHttp2DataFrame(stream=3, content=UnpooledSlicedByteBuf(ridx: 0, widx: 33, cap: 33/33, unwrapped: PooledUnsafeDirectByteBuf(ridx: 150, widx: 150, cap: 179)), endStream=true, padding=0)

Question

  1. Did I send the header and data well?
  2. How can i convert this part to String
DefaultHttp2DataFrame(stream=3, content=UnpooledSlicedByteBuf(ridx: 0, widx: 33, cap: 33/33, unwrapped: PooledUnsafeDirectByteBuf(ridx: 150, widx: 150, cap: 179)), endStream=true, padding=0)

If you know the solution, please help me.

  • I am not sure I fully understand the problem but try adding LoggingHandler(LogLevel.INFO) to the ChannelInitializer. It will provide you a nice hex (with strings) output of the communicating. I would suggest disabling this when you go into production – Haim Raman Feb 11 '21 at 08:04
  • Thank you for your help. I can check that the response is coming correctly using the logger. I think I just need to convert to String. – izagood Feb 15 '21 at 01:31

1 Answers1

0

Answer myself.

Http2ResponseHandler.java

public final class ResponseHandler extends SimpleChannelInboundHandler<Http2StreamFrame> {

    private final CountDownLatch latch = new CountDownLatch(1);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Http2StreamFrame msg) throws Exception {
        if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) {
            
            DefaultHttp2DataFrame dataFrame = (DefaultHttp2DataFrame) msg;
            ByteBuf dataContent = dataFrame.content();      
            String data = dataContent.toString(Charset.forName("utf-8"));
            System.out.println(data);
            
            latch.countDown();
        } else if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) {
            
            DefaultHttp2HeadersFrame headerFrame = (DefaultHttp2HeadersFrame) msg;
            DefaultHttp2Headers header = (DefaultHttp2Headers) headerFrame.headers();
            System.out.println(header.get("apns-id"));
            
            latch.countDown();
        }
    }
    /**
     * Waits for the latch to be decremented (i.e. for an end of stream message to be received), or for
     * the latch to expire after 5 seconds.
     * @return true if a successful HTTP/2 end of stream message was received.
     */
    public boolean responseSuccessfullyCompleted() {
        try {
            return latch.await(5, TimeUnit.SECONDS);
        } catch (InterruptedException ie) {
            System.err.println("Latch exception: " + ie.getMessage());
            return false;
        }
    }

}

Question

  1. Did I send the header and data well?

-> Answer

final Http2Headers headers = new DefaultHttp2Headers();
headers.method(HttpMethod.POST.asciiName());
headers.scheme(HttpScheme.HTTPS.name());
headers.path(PATH + notification.getToken());
headers.add("apns-topic", topic);
headers.add("key", "value");

// if you have a data frame you have to put false.
final Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers, false);
  1. How can i convert this part to String

-> Answer

DefaultHttp2DataFrame dataFrame = (DefaultHttp2DataFrame) msg;
ByteBuf dataContent = dataFrame.content();      
String data = dataContent.toString(Charset.forName("utf-8"));

I hope it will be helpful to people who have the same curiosity as me.