I am migrating a distributed systems codebase from SOAP (JAX-WS) to gRPC-java. We use this codebase to teach remote calls, fault tolerance, security implementations.
On the JAX-WS architecture, there is an interceptor class (called a SOAP handler) that can intercept SOAP messages. You can configure handlers on the client and on the server.
For reference, this is a full sequence for a remote call on JAX-WS:
- Client - create port (stub) and invoke remote method
- Stub - convert Java objects to SOAP message (XML)
- ClientHandler - intercepts outgoing SOAP message and can read/write on it
- Network - SOAP request message transmitted
- ServerHandler - intercepts incoming SOAP message, can read/write
- Tie - convert SOAP message to Java objects
- Server - execute method, respond
- ServerHandler - intercepts outgoing SOAP response, can read/write
- Network - SOAP response message transmitted
- Client - create port (stub) and invoke remote method
- Stub - convert Java objects to SOAP message (XML)
- ClientHandler - intercepts incoming SOAP message
- Client - receives response
With this approach, we can create handlers to log SOAP messages, and to add security, like digital signature or encryption.
I am trying to have similar capabilities with gRPC on Java (v1.17.2).
I based my gRPC code in this google tutorial, a simple hello world with a unary method.
Based on these examples, I have written a ClientInterceptor:
package example.grpc.client;
import java.util.Set;
import io.grpc.*;
public class HelloClientInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> methodDescriptor,
CallOptions callOptions, Channel channel) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
channel.newCall(methodDescriptor, callOptions)) {
@Override
public void sendMessage(ReqT message) {
System.out.printf("Sending method '%s' message '%s'%n", methodDescriptor.getFullMethodName(),
message.toString());
super.sendMessage(message);
}
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
System.out.println(HelloClientInterceptor.class.getSimpleName());
ClientCall.Listener<RespT> listener = new ForwardingClientCallListener<RespT>() {
@Override
protected Listener<RespT> delegate() {
return responseListener;
}
@Override
public void onMessage(RespT message) {
System.out.printf("Received message '%s'%n", message.toString());
super.onMessage(message);
}
};
super.start(listener, headers);
}
};
}
}
I have created a ServerInterceptor:
package example.grpc.server;
import java.util.Set;
import io.grpc.*;
public class HelloServerInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata,
ServerCallHandler<ReqT, RespT> serverCallHandler) {
// print class name
System.out.println(HelloServerInterceptor.class.getSimpleName());
return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);
}
}
Here are (finally) my questions:
- How can the server interceptor see the message before and after the method execution?
- How can the server interceptor modify the message?
- How can the client interceptor modify the message?
The end-goal is to be able to write a CipherClientHandler and a CipherServerHandler that would encrypt the message bytes on the wire. I know that TLS is the right way to do it in practice, but I want students to do a custom implementation.
Thanks for any pointers in the right direction!