[Java] How to use SSL and Google Protobuf on Netty?

[Java] How to use SSL and Google Protobuf on Netty?

Recently, I tried to use Netty framework dealing with message transferring between different virtual machines which are including one server and some clients. Meanwhile, I need to use SSL mutual authentication and Google protobuf when these VMs do communication cross the internet. Although I had already encountered the term “SSL” a lot of times, I soon realised that I had never fully understood how it really works and how it could be implemented in Java.

I would like to share my experience about implementing SSL plus Google Protobuf on Netty. But I wouldn’t exactly tell you what Netty, SSL and Google protobuf are.

Prepare Keystore files

First of all, we need to create the keystore file and certificate for server by Java keytool:

$ keytool -genkey -alias nettyserver -keysize 2048 -validity 365 -keyalg RSA -keypass password -storepass password -keystore nettyserver.jks

Then, export the server’s certificate from nettyserver.jks:

$ keytool -export -alias nettyserver -keystore nettyserver.jks -storepass password -keypass password -file nettyserver.crt

Create keystore file for client:

$ keytool -genkey -alias nettyclient -keysize 2048 -validity 365 -keyalg RSA -keypass password -storepass password -keystore nettyclient.jks

Export the client’s certificate from nettyclient.jks:

$ keytool -export -alias nettyclient -keystore nettyclient.jks -storepass password -keypass password -file nettyclient.crt

Because client need to do authentication with server, we need to import the server’s certificate nettyserver.crt into the client’s keystort file nettyclient.jks:

$ keytool -import -trustcacerts -alias nettyserver -file nettyserver.crt -storepass password -keystore nettyclient.jks

Vice versa, we need to import the client’s certificate into the server’s keystore file:

$ keytool -import -trustcacerts -alias nettyclient -file nettyclient.crt -storepass password -keystore nettyserver.jks

Install Google Protobuf3 complier as well as prepare Protobuf and Netty libraries

You can download Google Protobuf source code from here and do cross complier. If you don’t want do cross complier, you can simply download files from here basing on your OS platform.

After you install google protobuf complier, you should be able to execute $ protoc --version command. You also need to put Google Protobuf Java library into your project (Don’t forget the netty5 library). Of course, make sure their version are the same. I strongly recommend you should read Google protobuf official tutorials, it helps you to understand how to use it.

In addition, if you don’t want to add libraries into project manually, you can include protobuf and netty into maven local repository.

    <dependencies>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.0.0-beta-2</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>5.0.0.Alpha2</version>
        </dependency>
    </dependencies>

Defining Your Protocol Format

According to google official tutorials, we need to start with a .proto file. Here is the .proto file that defines your messages, addressbook.proto:

syntax = "proto3";
package tutorials;

option java_package = "com.tutorials.protobuf";
option java_outer_classname = "AddressBookProtos";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  PhoneNumber phone = 4;
}

message AddressBook {
  Person person = 1;
}

Now run the compiler, specifying the source directory (where your application’s source code lives – the current directory is used if you don’t provide a value), the destination directory (where you want the generated code to go; often the same as $SRC_DIR), and the path to your .proto. In this case, you:

$ protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

If you generate code successfully, you will find a AddressBookProtos.java in the destination directory.

Now, let’s move to part 4: implementation.

Implement SSL, Google Protobuf and Netty

First, create NettySimpleServer.java and prepare a channel for server:

public class NettySimpleServer {
    public static void main(String[] args) throws Exception {
        new NettySimpleServer(port).run();
    }

    static public int port = 5001;

    public NettySimpleServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        //NioEventLoopGroup is a multithreaded event loop that handles I/O operation.
        EventLoopGroup bossGroup = new NioEventLoopGroup(); 
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //ServerBootstrap is a helper class that sets up a server.
            ServerBootstrap b = new ServerBootstrap();  
            b.option(ChannelOption.SO_KEEPALIVE, true).group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class).childHandler(new SSLserverInitializer());

            System.out.println("Start server and wait connection...");
            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

Second, create SSLserverInitializer.java. We use this class to handle SSL authentication and encode/decode protobuf:

public class SSLserverInitializer extends ChannelInitializer {
    //The ChannelInitializer is a special handler that is purposed to help a user configure a new Channel. 
    //It is most likely that you want to configure the ChannelPipeline of the new Channel by adding some handlers.

    @Override public void initChannel(SocketChannel ch) throws Exception {
    ChannelPipeline pipeline = ch.pipeline();

    String password = "password";

    KeyStore ks = KeyStore.getInstance("JKS");
    // Use nettyserver.jks do client side authentication
    ks.load(ClassLoader.class.getResourceAsStream(File.separator + "nettyserver.jks"),
        password.toCharArray());

    TrustManagerFactory tmFactory = TrustManagerFactory.getInstance("SunX509");
    tmFactory.init(ks);

    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(ks, password.toCharArray());

    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), tmFactory.getTrustManagers(), null);

    SSLEngine sslEngine = sslContext.createSSLEngine();
    sslEngine.setUseClientMode(false);
    sslEngine.setNeedClientAuth(true);
    sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols());
    sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites());
    sslEngine.setEnableSessionCreation(true);

    // Add SSL handler into pipeline
    pipeline.addFirst("SSL", new SslHandler(sslEngine));

    // Add protobuf handler into pipeline
    pipeline.addLast(new ProtobufVarint32FrameDecoder());
    pipeline.addLast(new ProtobufDecoder(AddressBookProtos.Person.getDefaultInstance()));
    pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
    pipeline.addLast(new ProtobufEncoder());

    // Add custom handler
    pipeline.addLast(new SSLserverHandler());
    }
}

Third, create SSLserverHandler.java to handle channel operation:

public class SSLserverHandler extends ChannelHandlerAdapter {
    //the channelActive() method will be invoked when a connection is established and ready to generate traffic
        
    @Override public void channelActive(final ChannelHandlerContext ctx) {
        //Send protobuf to client side
        ctx.writeAndFlush(sendPersonalInfo());
    }

    @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    // Set personal info into AddressBookProtos that be created from Google protobuf complier
    private AddressBookProtos.Person sendPersonalInfo() {
        AddressBookProtos.Person.Builder builder = AddressBookProtos.Person.newBuilder();
        builder.setId(10000);
        builder.setEmail("bluebottle@mail.com");
        builder.setName("bluebottle");
        builder.setPhone(AddressBookProtos.Person.PhoneNumber.newBuilder()
               .setNumber("5555-4321")
               .setType(AddressBookProtos.Person.PhoneType.HOME));

        return builder.build();
        }
    }

After implementing server, we now begin to implement client NettySimpleClient.java. Now, I wouldn’t divide client’s code into separate parts. I just put them together into one class.

public class NettySimpleClient {
    public static void main(String[] args) throws Exception {
        new NettySimpleClient("localhost", 5001).run();
    }

    private String host;
    private int port;

    public NettySimpleClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup).channel(NioSocketChannel.class)
                    .handler(new SSLclientInitializer());

            // Start the client.
            System.out.println("Start client and connect to server...");
            ChannelFuture f = b.connect(host, port).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }

    public class SSLclientHandler extends ChannelHandlerAdapter {
        //Received Data
        @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("# channelRead");

            AddressBookProtos.Person person = (AddressBookProtos.Person) msg;
            System.out.println("id: " + person.getId());
            System.out.println("name: " + person.getName());
            System.out.println("email: " + person.getEmail());
            System.out.println("phone: " + person.getPhone().getNumber());
            System.out.println("phone type: " + person.getPhone().getType().toString());
        }

        @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }

    public class SSLclientInitializer extends ChannelInitializer<SocketChannel> {
        @Override public void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();

            String password = "password";

            KeyStore ks = KeyStore.getInstance("JKS");
            ks.load(ClassLoader.class.getResourceAsStream(File.separator + "nettyclient.jks"),
                    password.toCharArray());

            TrustManagerFactory tmFactory = TrustManagerFactory.getInstance("SunX509");
            tmFactory.init(ks);

            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            kmf.init(ks, password.toCharArray());

            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers(), tmFactory.getTrustManagers(), null);

            SSLEngine sslEngine = sslContext.createSSLEngine();
            sslEngine.setUseClientMode(true);

            sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols());
            sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites());
            sslEngine.setEnableSessionCreation(true);

            pipeline.addFirst("SSL", new SslHandler(sslEngine));

            // Add protobuf
            pipeline.addLast(new ProtobufVarint32FrameDecoder());
            pipeline.addLast(new ProtobufDecoder(AddressBookProtos.Person.getDefaultInstance()));
            pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
            pipeline.addLast(new ProtobufEncoder());

            // and then business logic.
            pipeline.addLast(new SSLclientHandler());
        }
    }
}

When we run server and then run client, we can see the below messages that will be printed in client’s console:

Start client and connect to server...
# channelRead
id:10000
name:bluebottle
email:bluebottle@mail.com
phone:5555-4321
phone type:HOME

Finally, if you would like to understand what the netty is, I recommend you can read “Netty in Action“. Or you can read my Netty introduction slides to understand Netty roughly.

(Visited 589 time, 1 visit today)
Facebooktwittergoogle_plusredditpinterestlinkedinmail
Comments are closed.