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:
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:
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!
By "method execution", I'm assuming you're meaning "Server - execute method, respond" from earlier. The exact time the server method is called is not part of the interception API and should not be depended on. With the async server handlers today, it happens that the server's method is called when serverListener.halfClose()
is called. But again, that should not be depended on. It's unclear why this is necessary.
The server interceptor receives the ReqT message
for the request and the RespT message
for the response. To modify the messages, simply modify those messages before calling super
.
The client interceptor can do the same as the server interceptor; modify the message before passing it along.
Note that when I say "modify the message", it would generally be implemented as "make a copy of the message with the appropriate modifications."
But if you are going to want to encrypt/decrypt the messages, that doesn't flow as easily from the API because you are completely changing their type. You are given a ReqT
and you will turn that into bytes. To do this you must modify the MethodDescriptor
s.
On client-side this can be done within start()
and providing your own Marshaller
s to MethodDescriptor.Builder
. You have access to the application's original MethodDescriptor
, so you can use that to serialize to bytes.
Marshaller ENCRYPTING_MARSHALLER = new Marshaller<InputStream>() {
@Override
public InputStream parse(InputStream stream) {
return decrypt(stream);
}
@Override
public InputStream stream(InputStream stream) {
return encrypt(stream);
}
};
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> methodDescriptor,
CallOptions callOptions, Channel channel) {
ClientCall<InputStream, InputStream> call = channel.newCall(
methodDescriptor.toBuilder(
ENCRYPTING_MARSHALLER, ENCRYPTING_MARSHALLER),
callOptions);
// Can't use Forwarding* because the generics would break.
// Note that all of this is basically boilerplate; the marshaller is
// doing the work.
return new ClientCall<ReqT, RespT>() {
@Override
public void halfClose() {
call.halfClose();
}
// ... ditto for _all_ the other methods on ClientCall
@Override
public void sendMessage(ReqT message) {
call.sendMessage(methodDescriptor.streamRequest(message));
}
@Override
public void start(Listener<RespT> listener, Metadata headers) {
call.start(new Listener<InputStream>() {
@Override
public void onHalfClose() {
listener.onHalfClose();
}
// ... ditto for _all_ the other methods on Listener
@Override
public void onMessage(InputStream message) {
listener.onMessage(methodDescriptor.parseResponse(message));
}
}, headers);
}
};
}
Server-side would normally be similar but a bit more complicated, since you would need to rebuild the ServerServiceDefinition
which can't be done as a normal interceptor. But there happens to be a utility that does the boilerplate:
ssd = ServerInterceptors.useMarshalledMessages(ssd, ENCRYPTING_MARSHALLER);
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.