/*
* Copyright (c) 2012 Mike Heath. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cloudeventbus.server;
import cloudeventbus.Constants;
import cloudeventbus.Subject;
import cloudeventbus.codec.AuthenticationRequestFrame;
import cloudeventbus.codec.AuthenticationResponseFrame;
import cloudeventbus.codec.DecodingException;
import cloudeventbus.codec.ErrorFrame;
import cloudeventbus.codec.Frame;
import cloudeventbus.codec.GreetingFrame;
import cloudeventbus.codec.PingFrame;
import cloudeventbus.codec.PongFrame;
import cloudeventbus.codec.PublishFrame;
import cloudeventbus.codec.ServerReadyFrame;
import cloudeventbus.codec.SubscribeFrame;
import cloudeventbus.codec.UnsubscribeFrame;
import cloudeventbus.hub.SubscribeableHub;
import cloudeventbus.hub.SubscriptionHandle;
import cloudeventbus.pki.CertificateChain;
import cloudeventbus.pki.CertificatePermissionError;
import cloudeventbus.pki.CertificateUtils;
import cloudeventbus.pki.InvalidCertificateException;
import cloudeventbus.pki.InvalidSignatureException;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundMessageHandlerAdapter;
import io.netty.channel.EventLoop;
import io.netty.handler.codec.DecoderException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* @author Mike Heath <elcapo@gmail.com>
*/
public class ServerHandler extends ChannelInboundMessageHandlerAdapter<Frame> {
private static final Logger LOGGER = LoggerFactory.getLogger(ServerHandler.class);
private final ServerConfig serverConfig;
private final ClusterManager clusterManager;
private final GlobalHub hub;
private final SubscribeableHub<Frame> clientSubscriptionHub;
private byte[] challenge;
private boolean serverReady = false;
private CertificateChain clientCertificates;
private String clientAgent;
private long clientId;
private boolean serverConnection;
// Subscription handler fields
private NettyHandler handler;
private final Map<Subject, SubscriptionHandle> subscriptionHandles = new HashMap<>();
// Ping and idle detection fields
private Runnable idleTask;
private ScheduledFuture<?> idleFuture;
private Runnable pingTask;
private ScheduledFuture<?> pingFuture;
public ServerHandler(ServerConfig serverConfig, ClusterManager clusterManager, GlobalHub hub, SubscribeableHub<Frame> clientSubscriptionHub) {
this.serverConfig = serverConfig;
this.clusterManager = clusterManager;
this.hub = hub;
this.clientSubscriptionHub = clientSubscriptionHub;
}
@Override
public void messageReceived(ChannelHandlerContext context, Frame frame) throws Exception {
resetIdleTask(context.channel().eventLoop());
LOGGER.debug("Received frame on server: {}", frame);
switch (frame.getFrameType()) {
case AUTH_RESPONSE: {
AuthenticationResponseFrame authenticationResponse = (AuthenticationResponseFrame) frame;
final CertificateChain certificates = authenticationResponse.getCertificates();
serverConfig.getTrustStore().validateCertificateChain(certificates);
this.clientCertificates = certificates;
CertificateUtils.validateSignature(
certificates.getLast().getPublicKey(),
challenge,
authenticationResponse.getSalt(),
authenticationResponse.getDigitalSignature());
switch (certificates.getLast().getType()) {
case AUTHORITY:
throw new InvalidCertificateException("Can not use an authority certificate to authenticate to server.");
case CLIENT:
serverConnection = false;
break;
case SERVER:
serverConnection = true;
clusterManager.addPeer(new ServerPeer(clientId, context.channel()));
break;
}
serverReady = true;
context.write(ServerReadyFrame.SERVER_READY);
break;
}
case AUTHENTICATE: {
if (!serverConfig.hasSecurityCredentials()) {
throw new CloudEventBusServerException("Unable to authenticate with server, missing private key or certificate chain");
}
final AuthenticationRequestFrame authenticationRequest = (AuthenticationRequestFrame) frame;
final byte[] salt = CertificateUtils.generateChallenge();
final byte[] signature = CertificateUtils.signChallenge(serverConfig.getPrivateKey(), authenticationRequest.getChallenge(), salt);
AuthenticationResponseFrame authenticationResponse = new AuthenticationResponseFrame(serverConfig.getCertificateChain(), salt, signature);
context.write(authenticationResponse);
break;
}
case GREETING:
final GreetingFrame greetingFrame = (GreetingFrame) frame;
clientAgent = greetingFrame.getAgent();
clientId = greetingFrame.getId();
if (greetingFrame.getVersion() != Constants.PROTOCOL_VERSION) {
throw new InvalidProtocolVersionException("This server doesn't support protocol version " + greetingFrame.getVersion());
}
// TODO Try moving this back to channelActive and see if server still crashes...
context.write(new GreetingFrame(Constants.PROTOCOL_VERSION, serverConfig.getAgentString(), serverConfig.getId()));
if (serverConfig.getTrustStore() == null) {
serverReady = true;
context.write(ServerReadyFrame.SERVER_READY);
} else {
challenge = CertificateUtils.generateChallenge();
context.write(new AuthenticationRequestFrame(challenge));
}
break;
case PONG:
// Do nothing.
break;
default:
if (!serverReady) {
throw new ServerNotReadyException("This server requires authentication.");
} else {
switch (frame.getFrameType()) {
case PUBLISH: {
final PublishFrame publishFrame = (PublishFrame) frame;
final Subject subject = publishFrame.getSubject();
final String body = publishFrame.getBody();
if (clientCertificates != null) {
clientCertificates.getLast().validatePublishPermission(subject);
}
final Subject replySubject = publishFrame.getReplySubject();
// Implicitly subscribe to request reply subjects
if (replySubject != null && replySubject.isRequestReply()) {
clientSubscriptionHub.subscribe(replySubject, handler);
}
// If the publish is coming from a peer server, publish locally
if (serverConnection) {
hub.publish(subject, replySubject, body);
} else {
hub.broadcast(subject, replySubject, body);
}
break;
}
case SUBSCRIBE: {
final SubscribeFrame subscribeFrame = (SubscribeFrame) frame;
final Subject subject = subscribeFrame.getSubject();
if (clientCertificates != null) {
clientCertificates.getLast().validateSubscribePermission(subject);
}
if (subscriptionHandles.containsKey(subject)) {
throw new DuplicateSubscriptionException("Already subscribed to subject " + subject);
}
// If the connection is a peer server, let the ClusterManager forward messages instead of the normal subscription mechanism
if (!serverConnection) {
final SubscriptionHandle subscriptionHandle = clientSubscriptionHub.subscribe(subject, handler);
subscriptionHandles.put(subject, subscriptionHandle);
}
break;
}
case UNSUBSCRIBE: {
final UnsubscribeFrame unsubscribeFrame = (UnsubscribeFrame) frame;
final Subject subject = unsubscribeFrame.getSubject();
final SubscriptionHandle subscriptionHandle = subscriptionHandles.get(subject);
if (subscriptionHandle == null) {
throw new NotSubscribedException("Not subscribed to subject " + subject);
}
subscriptionHandle.remove();
break;
}
case PING:
context.write(PongFrame.PONG);
break;
default:
throw new CloudEventBusServerException("Unable to handle frame of type " + frame.getClass().getName());
}
}
}
}
private void resetIdleTask(EventLoop eventLoop) {
try {
if (idleFuture != null) {
idleFuture.cancel(false);
}
if (pingFuture != null) {
pingFuture.cancel(false);
}
idleFuture = eventLoop.schedule(idleTask, 1, TimeUnit.MINUTES);
pingFuture = eventLoop.schedule(pingTask, 30, TimeUnit.SECONDS);
} catch (UnsupportedOperationException e) {
// Don't throw an error when running tests.
LOGGER.warn("Ping and idle close not supported", e);
}
}
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
LOGGER.debug("Channel active from {}", ctx.channel().remoteAddress());
idleTask = new Runnable() {
@Override
public void run() {
LOGGER.warn("Idle connection {}", ctx.channel().remoteAddress());
error(ctx, new ErrorFrame(ErrorFrame.Code.IDLE_TIMEOUT, "Connection closed for idle timeout"));
}
};
pingTask = new Runnable() {
@Override
public void run() {
ctx.write(PingFrame.PING);
}
};
resetIdleTask(ctx.channel().eventLoop());
handler = new NettyHandler(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
LOGGER.debug("Channel inactive from {}", ctx.channel().remoteAddress());
// Cleanup subscriptions in hub
for (SubscriptionHandle handle : subscriptionHandles.values()) {
handle.remove();
}
// Cancel idle check and ping tasks.
if (idleFuture != null) {
idleFuture.cancel(false);
}
if (pingFuture != null) {
pingFuture.cancel(false);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// TODO Experiment with using a marker to identify the remote agent.
LOGGER.error((clientAgent == null ? "" : clientAgent + " ") + cause.getMessage(), cause);
if (cause instanceof DecoderException) {
cause = cause.getCause();
}
//TODO Move error code to CloudEventBus exception
final ErrorFrame.Code errorCode;
if (cause instanceof DecodingException) {
errorCode = ErrorFrame.Code.MALFORMED_REQUEST;
} else if (cause instanceof InvalidSignatureException) {
errorCode = ErrorFrame.Code.INVALID_SIGNATURE;
} else if (cause instanceof ServerNotReadyException) {
errorCode = ErrorFrame.Code.SERVER_NOT_READY;
} else if (cause instanceof InvalidCertificateException) {
errorCode = ErrorFrame.Code.INVALID_CERTIFICATE;
} else if (cause instanceof InvalidProtocolVersionException) {
errorCode = ErrorFrame.Code.UNSUPPORTED_PROTOCOL_VERSION;
} else if (cause instanceof DuplicateSubscriptionException) {
errorCode = ErrorFrame.Code.DUPLICATE_SUBSCRIPTION;
} else if (cause instanceof NotSubscribedException) {
errorCode = ErrorFrame.Code.NOT_SUBSCRIBED;
} else if (cause instanceof CertificatePermissionError) {
errorCode = ErrorFrame.Code.INSUFFICIENT_PRIVILEGES;
} else {
errorCode = ErrorFrame.Code.SERVER_ERROR;
}
final ErrorFrame errorFrame = new ErrorFrame(errorCode, cause.getMessage());
error(ctx, errorFrame);
}
private void error(ChannelHandlerContext ctx, ErrorFrame errorFrame) {
ctx.write(errorFrame).addListener(ChannelFutureListener.CLOSE);
}
}