/*
* Copyright 2014-2016 CyberVision, Inc.
*
* 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 org.kaaproject.kaa.server.operations.service.akka.actors.io;
import akka.actor.ActorContext;
import akka.actor.ActorRef;
import org.kaaproject.kaa.common.dto.credentials.CredentialsDto;
import org.kaaproject.kaa.common.dto.credentials.CredentialsStatus;
import org.kaaproject.kaa.common.dto.credentials.EndpointRegistrationDto;
import org.kaaproject.kaa.common.endpoint.security.KeyUtil;
import org.kaaproject.kaa.common.endpoint.security.MessageEncoderDecoder;
import org.kaaproject.kaa.common.hash.EndpointObjectHash;
import org.kaaproject.kaa.server.common.Base64Util;
import org.kaaproject.kaa.server.common.dao.exception.CredentialsServiceException;
import org.kaaproject.kaa.server.common.dao.exception.EndpointRegistrationServiceException;
import org.kaaproject.kaa.server.common.thrift.gen.operations.RedirectionRule;
import org.kaaproject.kaa.server.node.service.credentials.CredentialsService;
import org.kaaproject.kaa.server.node.service.credentials.CredentialsServiceLocator;
import org.kaaproject.kaa.server.node.service.registration.RegistrationService;
import org.kaaproject.kaa.server.operations.service.akka.AkkaContext;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.endpoint.SyncRequestMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.io.response.NettySessionResponseMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.io.response.SessionResponse;
import org.kaaproject.kaa.server.operations.service.cache.CacheService;
import org.kaaproject.kaa.server.operations.service.metrics.MeterClient;
import org.kaaproject.kaa.server.operations.service.metrics.MetricsService;
import org.kaaproject.kaa.server.sync.ClientSync;
import org.kaaproject.kaa.server.sync.ClientSyncMetaData;
import org.kaaproject.kaa.server.sync.RedirectServerSync;
import org.kaaproject.kaa.server.sync.ServerSync;
import org.kaaproject.kaa.server.sync.SyncStatus;
import org.kaaproject.kaa.server.sync.platform.PlatformEncDec;
import org.kaaproject.kaa.server.sync.platform.PlatformEncDecException;
import org.kaaproject.kaa.server.sync.platform.PlatformLookup;
import org.kaaproject.kaa.server.transport.EndpointVerificationError;
import org.kaaproject.kaa.server.transport.EndpointVerificationException;
import org.kaaproject.kaa.server.transport.InvalidSdkTokenException;
import org.kaaproject.kaa.server.transport.channel.ChannelContext;
import org.kaaproject.kaa.server.transport.message.ErrorBuilder;
import org.kaaproject.kaa.server.transport.message.Message;
import org.kaaproject.kaa.server.transport.message.MessageBuilder;
import org.kaaproject.kaa.server.transport.message.SessionAwareMessage;
import org.kaaproject.kaa.server.transport.message.SessionInitMessage;
import org.kaaproject.kaa.server.transport.session.SessionAware;
import org.kaaproject.kaa.server.transport.session.SessionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class EncDecActorMessageProcessor {
private static final Logger LOG = LoggerFactory.getLogger(EncDecActorMessageProcessor.class);
private final CacheService cacheService;
private final CredentialsServiceLocator credentialsServiceLocator;
private final RegistrationService registrationService;
private final MessageEncoderDecoder crypt;
private final Map<Integer, PlatformEncDec> platformEncDecMap;
private final Boolean supportUnencryptedConnection;
private final ActorRef opsActor;
private final MeterClient sessionInitMeter;
private final MeterClient sessionRequestMeter;
private final MeterClient sessionResponseMeter;
private final MeterClient redirectMeter;
private final MeterClient errorMeter;
protected EncDecActorMessageProcessor(ActorRef epsActor,
AkkaContext context,
Set<String> platformProtocols) {
super();
this.opsActor = epsActor;
this.cacheService = context.getCacheService();
this.credentialsServiceLocator = context.getCredentialsServiceLocator();
this.registrationService = context.getRegistrationService();
this.supportUnencryptedConnection = context.getSupportUnencryptedConnection();
this.crypt = new MessageEncoderDecoder(
context.getKeyStoreService().getPrivateKey(), context.getKeyStoreService().getPublicKey());
this.platformEncDecMap = PlatformLookup.initPlatformProtocolMap(platformProtocols);
MetricsService metricsService = context.getMetricsService();
this.sessionInitMeter = metricsService.createMeter(
"sessionInitMeter", Thread.currentThread().getName());
this.sessionRequestMeter = metricsService.createMeter(
"sessionRequestMeter", Thread.currentThread().getName());
this.sessionResponseMeter = metricsService.createMeter(
"sessionResponseMeter", Thread.currentThread().getName());
this.redirectMeter = metricsService.createMeter(
"redirectMeter", Thread.currentThread().getName());
this.errorMeter = metricsService.createMeter(
"errorMeter", Thread.currentThread().getName());
}
void decodeAndForward(ActorContext context, SessionInitMessage message) {
try {
sessionInitMeter.mark();
processSessionInitRequest(context, message);
} catch (Exception ex) {
processErrors(message.getChannelContext(), message.getErrorBuilder(), ex);
}
}
void decodeAndForward(ActorContext context, SessionAwareMessage message) {
try {
sessionRequestMeter.mark();
processSessionRequest(context, message);
} catch (Exception ex) {
processErrors(message.getChannelContext(), message.getErrorBuilder(), ex);
}
}
void encodeAndReply(SessionResponse message) {
try {
sessionResponseMeter.mark();
if (message.getError() == null) {
processSessionResponse(message);
} else {
processErrors(message.getChannelContext(), message.getErrorBuilder(), message.getError());
}
} catch (Exception ex) {
processErrors(message.getChannelContext(), message.getErrorBuilder(), ex);
}
}
/**
* Forward message to OperationsServerActor.
*
*/
public void forward(ActorContext context, SessionAware message) {
if (isSdkTokenValid(message.getSessionInfo().getSdkToken())) {
LOG.debug("Forwarding session aware message: {}", message);
this.opsActor.tell(message, context.self());
} else {
LOG.debug("Session aware message ignored. Reason: message {} has invalid sdk token",
message);
}
}
void redirect(RedirectionRule redirection, SessionInitMessage message) {
try {
redirectMeter.mark();
ClientSync request = decodeRequest(message);
ServerSync response = buildRedirectionResponse(redirection, request);
EndpointObjectHash key = getEndpointObjectHash(request);
String sdkToken = getSdkToken(request);
String appToken = getAppToken(sdkToken);
SessionInfo sessionInfo = new SessionInfo(
message.getChannelUuid(), message.getPlatformId(), message.getChannelContext(),
message.getChannelType(), crypt.getSessionCipherPair(), key, appToken,
sdkToken, message.getKeepAlive(),
message.isEncrypted());
SessionResponse responseMessage = new NettySessionResponseMessage(
sessionInfo, response, message.getMessageBuilder(),
message.getErrorBuilder());
LOG.debug("Redirect Response: {}", response);
processSessionResponse(responseMessage);
} catch (Exception ex) {
processErrors(message.getChannelContext(), message.getErrorBuilder(), ex);
}
}
void redirect(RedirectionRule redirection, SessionAwareMessage message) {
try {
LOG.trace("Redirecting {} SessionAwareMessage", message);
redirectMeter.mark();
ClientSync request = decodeRequest(message);
ServerSync response = buildRedirectionResponse(redirection, request);
SessionInfo sessionInfo = message.getSessionInfo();
SessionResponse responseMessage = new NettySessionResponseMessage(
sessionInfo, response, message.getMessageBuilder(),
message.getErrorBuilder());
LOG.debug("Redirect Response: {}", response);
processSessionResponse(responseMessage);
} catch (Exception ex) {
processErrors(message.getChannelContext(), message.getErrorBuilder(), ex);
}
}
private ServerSync buildRedirectionResponse(RedirectionRule redirection, ClientSync request) {
RedirectServerSync redirectSyncResponse = new RedirectServerSync(
redirection.getAccessPointId());
ServerSync response = new ServerSync();
response.setRequestId(request.getRequestId());
response.setStatus(SyncStatus.REDIRECT);
response.setRedirectSync(redirectSyncResponse);
return response;
}
private void processSessionInitRequest(ActorContext context, SessionInitMessage message)
throws GeneralSecurityException, PlatformEncDecException,
InvalidSdkTokenException, EndpointVerificationException {
ClientSync request = decodeRequest(message);
EndpointObjectHash key = getEndpointObjectHash(request);
String sdkToken = getSdkToken(request);
if (isSdkTokenValid(sdkToken)) {
String appToken = getAppToken(sdkToken);
verifyEndpoint(key, appToken);
SessionInfo session = new SessionInfo(
message.getChannelUuid(), message.getPlatformId(), message.getChannelContext(),
message.getChannelType(), crypt.getSessionCipherPair(), key,
appToken, sdkToken, message.getKeepAlive(),
message.isEncrypted());
message.onSessionCreated(session);
forwardToOpsActor(context, session, request, message);
} else {
LOG.info("Invalid sdk token received: {}", sdkToken);
throw new InvalidSdkTokenException();
}
}
private void verifyEndpoint(EndpointObjectHash key, String appToken)
throws EndpointVerificationException {
// Credentials id match EP id in current implementation.
// We will have two dedicated variables with same value just to
// simplify reading of the logic.
String credentialsId = Base64Util.encode(key.getData());
String endpointId = credentialsId;
try {
Optional<EndpointRegistrationDto> registrationLookupResult = registrationService
.findEndpointRegistrationByCredentialsId(credentialsId);
if (!registrationLookupResult.isPresent()) {
String appId = cacheService.getApplicationIdByAppToken(appToken);
CredentialsService credentialsService = credentialsServiceLocator.getCredentialsService(
appId);
Optional<CredentialsDto> credentailsLookupResult = credentialsService.lookupCredentials(
credentialsId);
if (!credentailsLookupResult.isPresent()) {
LOG.info("[{}] Credentials with id: [{}] not found!", appToken, credentialsId);
throw new EndpointVerificationException(
EndpointVerificationError.NOT_FOUND, "Credentials not found!");
}
CredentialsDto credentials = credentailsLookupResult.get();
if (credentials.getStatus() == CredentialsStatus.REVOKED) {
LOG.info("[{}] Credentials with id: [{}] was already revoked!", appToken, credentialsId);
throw new EndpointVerificationException(
EndpointVerificationError.REVOKED, "Credentials was revoked!");
} else if (credentials.getStatus() == CredentialsStatus.IN_USE) {
LOG.info("[{}] Credentials with id: [{}] are already in use!", appToken, credentialsId);
throw new EndpointVerificationException(
EndpointVerificationError.IN_USE, "Credentials are already in use!");
} else {
credentialsService.markCredentialsInUse(credentialsId);
EndpointRegistrationDto endpointRegistration = new EndpointRegistrationDto();
endpointRegistration.setApplicationId(appId);
endpointRegistration.setCredentialsId(credentialsId);
endpointRegistration.setEndpointId(endpointId);
registrationService.saveEndpointRegistration(endpointRegistration);
}
} else {
EndpointRegistrationDto endpointRegistration = registrationLookupResult.get();
if (endpointRegistration.getEndpointId() == null) {
String appId = cacheService.getApplicationIdByAppToken(appToken);
CredentialsService credentialsService = credentialsServiceLocator.getCredentialsService(
appId);
credentialsService.markCredentialsInUse(credentialsId);
endpointRegistration.setEndpointId(endpointId);
registrationService.saveEndpointRegistration(endpointRegistration);
} else if (!endpointId.equals(endpointRegistration.getEndpointId())) {
LOG.info("[{}] Credentials with id: [{}] are already in use!",
appToken, credentialsId);
throw new EndpointVerificationException(EndpointVerificationError.IN_USE,
"Credentials are already in use!");
}
}
LOG.debug("[{}] Succesfully validated endpoint information: [{}]",
appToken, credentialsId);
} catch (CredentialsServiceException ex) {
LOG.info("[{}] Failed to lookup credentials info with id: [{}]", appToken, credentialsId, ex);
throw new RuntimeException(ex);
} catch (EndpointRegistrationServiceException ex) {
LOG.info("[{}] Failed to lookup registration info with id: [{}]",
appToken, credentialsId, ex);
throw new RuntimeException(ex);
}
}
private void processSessionRequest(ActorContext context, SessionAwareMessage message)
throws GeneralSecurityException, PlatformEncDecException, InvalidSdkTokenException {
ClientSync request = decodeRequest(message);
if (isSdkTokenValid(message.getSessionInfo().getSdkToken())) {
forwardToOpsActor(context, message.getSessionInfo(), request, message);
} else {
LOG.info("Invalid sdk token received: {}", message.getSessionInfo().getSdkToken());
throw new InvalidSdkTokenException();
}
}
private void forwardToOpsActor(ActorContext context,
SessionInfo session,
ClientSync request,
Message requestMessage) {
SyncRequestMessage message = new SyncRequestMessage(
session, request, requestMessage, context.self());
this.opsActor.tell(message, context.self());
}
private void processSessionResponse(SessionResponse message)
throws GeneralSecurityException, PlatformEncDecException {
SessionInfo session = message.getSessionInfo();
byte[] responseData = encodePlatformLevelData(message.getPlatformId(), message);
LOG.trace("Response data serialized");
if (session.isEncrypted()) {
crypt.setSessionCipherPair(session.getCipherPair());
responseData = crypt.encodeData(responseData);
LOG.trace("Response data crypted");
}
ChannelContext context = message.getSessionInfo().getCtx();
MessageBuilder converter = message.getMessageBuilder();
Object[] objects = converter.build(responseData, session.isEncrypted());
if (objects != null && objects.length > 0) {
for (Object object : objects) {
context.write(object);
}
context.flush();
}
}
private ClientSync decodeRequest(SessionInitMessage message)
throws GeneralSecurityException, PlatformEncDecException {
ClientSync syncRequest;
if (message.isEncrypted()) {
syncRequest = decodeEncryptedRequest(message);
} else if (supportUnencryptedConnection) {
syncRequest = decodeUnencryptedRequest(message);
} else {
LOG.warn("Received unencrypted init message, "
+ "but unencrypted connection forbidden by configuration.");
throw new GeneralSecurityException("Unencrypted connection forbidden by configuration.");
}
return syncRequest;
}
private ClientSync decodeRequest(SessionAwareMessage message)
throws GeneralSecurityException, PlatformEncDecException {
ClientSync syncRequest;
if (message.isEncrypted()) {
syncRequest = decodeEncryptedRequest(message);
} else if (supportUnencryptedConnection) {
syncRequest = decodeUnencryptedRequest(message);
} else {
LOG.warn("Received unencrypted aware message, "
+ "but unencrypted connection forbidden by configuration.");
throw new GeneralSecurityException("Unencrypted connection forbidden by configuration.");
}
return syncRequest;
}
private ClientSync decodeEncryptedRequest(SessionInitMessage message)
throws GeneralSecurityException, PlatformEncDecException {
byte[] requestRaw = crypt.decodeData(
message.getEncodedMessageData(), message.getEncodedSessionKey());
LOG.trace("Request data decrypted");
ClientSync request = decodePlatformLevelData(message.getPlatformId(), requestRaw);
LOG.trace("Request data deserialized");
PublicKey endpointKey = getPublicKey(request);
if (endpointKey == null) {
LOG.warn("Endpoint Key is null");
throw new GeneralSecurityException("Endpoint Key is null");
} else {
LOG.trace("Public key extracted");
}
crypt.setRemotePublicKey(endpointKey);
if (crypt.verify(message.getEncodedSessionKey(), message.getSessionKeySignature())) {
LOG.trace("Request data verified");
} else {
LOG.warn("Request data verification failed");
throw new GeneralSecurityException("Request data verification failed");
}
return request;
}
private ClientSync decodeEncryptedRequest(SessionAwareMessage message)
throws GeneralSecurityException, PlatformEncDecException {
SessionInfo session = message.getSessionInfo();
crypt.setSessionCipherPair(session.getCipherPair());
byte[] requestRaw = crypt.decodeData(message.getEncodedMessageData());
LOG.trace("Request data decrypted");
ClientSync request = decodePlatformLevelData(message.getPlatformId(), requestRaw);
LOG.trace("Request data deserialized");
return request;
}
private ClientSync decodeUnencryptedRequest(SessionInitMessage message)
throws GeneralSecurityException, PlatformEncDecException {
byte[] requestRaw = message.getEncodedMessageData();
LOG.trace("Try to convert raw data to SynRequest object");
ClientSync request = decodePlatformLevelData(message.getPlatformId(), requestRaw);
LOG.trace("Request data deserialized");
PublicKey endpointKey = getPublicKey(request);
if (endpointKey == null) {
LOG.warn("Endpoint Key is null");
throw new GeneralSecurityException("Endpoint Key is null");
} else {
LOG.trace("Public key extracted");
}
return request;
}
private ClientSync decodeUnencryptedRequest(SessionAwareMessage message)
throws PlatformEncDecException {
byte[] requestRaw = message.getEncodedMessageData();
ClientSync request = decodePlatformLevelData(message.getPlatformId(), requestRaw);
LOG.trace("Request data deserialized");
return request;
}
private byte[] encodePlatformLevelData(int platformId, SessionResponse message)
throws PlatformEncDecException {
PlatformEncDec encDec = platformEncDecMap.get(platformId);
if (encDec != null) {
return platformEncDecMap.get(platformId).encode(message.getResponse());
} else {
throw new PlatformEncDecException(
MessageFormat.format("Encoder for platform protocol [{0}] is not defined", platformId));
}
}
private ClientSync decodePlatformLevelData(Integer platformId, byte[] requestRaw)
throws PlatformEncDecException {
PlatformEncDec encDec = platformEncDecMap.get(platformId);
if (encDec != null) {
ClientSync syncRequest = platformEncDecMap.get(platformId).decode(requestRaw);
addAppTokenToClientSyncMetaData(syncRequest.getClientSyncMetaData());
return syncRequest;
} else {
throw new PlatformEncDecException(
MessageFormat.format("Decoder for platform protocol [{0}] is not defined", platformId));
}
}
private PublicKey getPublicKey(ClientSync request) throws GeneralSecurityException {
PublicKey endpointKey = null;
if (request.getProfileSync() != null
&& request.getProfileSync().getEndpointPublicKey() != null) {
byte[] publicKeySrc = request.getProfileSync().getEndpointPublicKey().array();
endpointKey = KeyUtil.getPublic(publicKeySrc);
}
if (endpointKey == null) {
EndpointObjectHash hash = getEndpointObjectHash(request);
endpointKey = cacheService.getEndpointKey(hash);
}
return endpointKey;
}
private void processErrors(ChannelContext ctx, ErrorBuilder converter, Exception ex) {
LOG.trace("Request processing failed", ex);
errorMeter.mark();
Object[] responses = converter.build(ex);
if (responses != null && responses.length > 0) {
for (Object response : responses) {
ctx.writeAndFlush(response);
}
} else {
ctx.fireExceptionCaught(ex);
}
}
private void addAppTokenToClientSyncMetaData(ClientSyncMetaData clientSyncMetaData) {
clientSyncMetaData.setApplicationToken(getAppToken(clientSyncMetaData.getSdkToken()));
}
private String getSdkToken(ClientSync request) {
return request.getClientSyncMetaData().getSdkToken();
}
private String getAppToken(String sdkToken) {
return cacheService.getAppTokenBySdkToken(sdkToken);
}
private boolean isSdkTokenValid(String sdkToken) {
return sdkToken != null && getAppToken(sdkToken) != null;
}
protected EndpointObjectHash getEndpointObjectHash(ClientSync request) {
return EndpointObjectHash.fromBytes(
request.getClientSyncMetaData().getEndpointPublicKeyHash().array());
}
}