/*
* Copyright 2013, The Sporting Exchange Limited
*
* 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 com.betfair.cougar.netutil.nio.marshalling;
import com.betfair.cougar.api.ExecutionContext;
import com.betfair.cougar.api.ExecutionContextWithTokens;
import com.betfair.cougar.api.RequestUUID;
import com.betfair.cougar.api.fault.CougarApplicationException;
import com.betfair.cougar.api.fault.FaultCode;
import com.betfair.cougar.api.geolocation.GeoLocationDetails;
import com.betfair.cougar.api.security.*;
import com.betfair.cougar.core.api.ServiceVersion;
import com.betfair.cougar.core.api.client.EnumWrapper;
import com.betfair.cougar.core.api.ev.ExecutionObserver;
import com.betfair.cougar.core.api.ev.ExecutionResult;
import com.betfair.cougar.core.api.ev.OperationKey;
import com.betfair.cougar.core.api.ev.TimeConstraints;
import com.betfair.cougar.core.api.exception.CougarClientException;
import com.betfair.cougar.core.api.exception.CougarException;
import com.betfair.cougar.core.api.exception.CougarFrameworkException;
import com.betfair.cougar.core.api.exception.ServerFaultCode;
import com.betfair.cougar.core.api.fault.FaultDetail;
import com.betfair.cougar.core.api.transcription.EnumUtils;
import com.betfair.cougar.core.api.transcription.Parameter;
import com.betfair.cougar.core.api.transcription.ParameterType;
import com.betfair.cougar.core.api.transcription.TranscriptionException;
import com.betfair.cougar.core.impl.DefaultTimeConstraints;
import com.betfair.cougar.core.impl.security.CertInfoExtractor;
import com.betfair.cougar.core.impl.security.CommonNameCertInfoExtractor;
import com.betfair.cougar.core.impl.security.SSLAwareTokenResolver;
import com.betfair.cougar.marshalling.api.socket.RemotableMethodInvocationMarshaller;
import com.betfair.cougar.netutil.nio.CougarProtocol;
import com.betfair.cougar.transport.api.RequestTimeResolver;
import com.betfair.cougar.transport.api.protocol.CougarObjectInput;
import com.betfair.cougar.transport.api.protocol.CougarObjectOutput;
import com.betfair.cougar.transport.api.protocol.http.ExecutionContextFactory;
import com.betfair.cougar.transport.api.protocol.socket.InvocationRequest;
import com.betfair.cougar.transport.api.protocol.socket.InvocationResponse;
import com.betfair.cougar.util.RequestUUIDImpl;
import com.betfair.cougar.util.geolocation.GeoIPLocator;
import com.betfair.cougar.util.geolocation.RemoteAddressUtils;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import javax.naming.NamingException;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Concrete implementation of @See RemotableMethodInvocationMarshaller class to
* transcribe requests/responses over the the binary socket transport
*/
public class SocketRMIMarshaller implements RemotableMethodInvocationMarshaller {
private final GeoIPLocator geoIpLocator;
private final IdentityTokenResolver<CougarObjectInput, CougarObjectOutput, X509Certificate[]> identityTokenResolver;
private final RequestTimeResolver<Long> requestTimeResolver;
private boolean hardFailEnumDeserialisation;
// For client side, as the GeoIpLocator/cert regex & request time resolver is only necessary server side.
public SocketRMIMarshaller() {
this(null,new CommonNameCertInfoExtractor(),null);
}
public SocketRMIMarshaller(GeoIPLocator geoIpLocator, CertInfoExtractor certInfoExtractor, RequestTimeResolver<Long> requestTimeResolver) {
this.geoIpLocator = geoIpLocator;
this.requestTimeResolver = requestTimeResolver;
this.identityTokenResolver = new SSLAwareTokenResolver<CougarObjectInput, CougarObjectOutput, X509Certificate[]>(certInfoExtractor) {
@Override
public List<IdentityToken> resolve(CougarObjectInput input, X509Certificate[] certificateChain) {
List<IdentityToken> tokens = new ArrayList<IdentityToken>();
try {
attachCertInfo(tokens, certificateChain);
} catch (NamingException e) {
throw new CougarFrameworkException("Unable to resolve cert info", e);
}
try {
int size = input.readInt();
for (int i = 0; i < size; i++) {
tokens.add(new IdentityToken(input.readString(), input.readString()));
}
return tokens;
}
catch (Exception e) {
throw new RuntimeException("Problem resolving IdentityTokens from CougarObjectInput...", e);
}
}
@Override
public void rewrite(List<IdentityToken> tokens, CougarObjectOutput output) {
try {
if (tokens == null) {
output.writeInt(0);
return;
}
output.writeInt(tokens.size());
for (IdentityToken token : tokens) {
output.writeString(token.getName());
output.writeString(token.getValue());
}
}
catch (Exception e) {
throw new RuntimeException("Problem transcribing IdentityTokens to CougarObjectOutput...", e);
}
}
@Override
public boolean isRewriteSupported() {
return true;
}
};
}
public static class InvocationResponseImpl implements InvocationResponse {
private final Object result;
private final CougarException exception;
public InvocationResponseImpl(final Object result) {
this(result, null);
}
public InvocationResponseImpl(final Object result, final CougarException exception) {
this.result = result;
this.exception = exception;
}
public void recreate(ExecutionObserver observer, ParameterType returnType, long size) {
if (isSuccess()) {
if (returnType.getImplementationClass().equals(EnumWrapper.class)) {
observer.onResult(new ExecutionResult(new EnumWrapper(returnType.getComponentTypes()[0].getImplementationClass(), (String)result)));
}
else {
observer.onResult(new ExecutionResult(result));
}
} else {
observer.onResult(new ExecutionResult(exception));
}
}
public boolean isSuccess() {
return exception == null;
}
public Object getResult() {
return result;
}
public CougarException getException() {
return exception;
}
}
@Override
public void writeInvocationRequest(InvocationRequest request, CougarObjectOutput out, IdentityResolver identityResolver, byte protocolVersion) throws IOException {
// todo: decide if we want app protocol versioning too?
// note that new additions to the app protocol must be backwards compatible from the client side, the server side response may be breaking since it knows what
// the client version is..
// if (protocolVersion >= CougarProtocol.TRANSPORT_PROTOCOL_VERSION_TIME_CONSTRAINTS) {
// out.writeBytes(new byte[] {CougarProtocol.TRANSPORT_PROTOCOL_VERSION_TIME_CONSTRAINTS});
// }
writeExecutionContext(request.getExecutionContext(), out, identityResolver, protocolVersion);
writeOperationKey(request.getOperationKey(), out);
writeArgs(request.getParameters(), request.getArgs(), out);
writeTimeConstraints(request.getTimeConstraints(), out, protocolVersion);
}
@Override
public void writeInvocationResponse(InvocationResponse response, CougarObjectOutput out, byte protocolVersion) throws IOException {
out.writeBoolean(response.isSuccess());
if (response.isSuccess()) {
// make sure we serialise enum responses as strings.. unless of course we're on an old version
if (protocolVersion >= CougarProtocol.TRANSPORT_PROTOCOL_VERSION_START_TLS && response.getResult() != null && Enum.class.isAssignableFrom(response.getResult().getClass())) {
out.writeObject(((Enum)response.getResult()).name());
}
else {
out.writeObject(response.getResult());
}
} else {
out.writeObject(response.getException().getServerFaultCode());
if (response.getException().getFault() != null) {
out.writeObject(response.getException().getFault().getDetail());
} else {
out.writeObject(null);
}
}
}
@Override
public InvocationResponse readInvocationResponse(ParameterType resultType, CougarObjectInput in) throws IOException {
EnumUtils.setHardFailureForThisThread(hardFailEnumDeserialisation);
try {
boolean success = in.readBoolean();
if (success) {
//A void return type is still marked by a value (which signifies null) on the wire, see
//TranscriptionStreamFactoryImpl for details
Object result = in.readObject();
return new InvocationResponseImpl(result);
} else {
ServerFaultCode code = (ServerFaultCode)in.readObject();
FaultDetail faultDetail = (FaultDetail) in.readObject();
if (faultDetail != null) {
if (faultDetail.getCause() != null) {
if (faultDetail.getCause() instanceof CougarApplicationException) {
return new InvocationResponseImpl(null, new CougarClientException(code, faultDetail.getDetailMessage(), (CougarApplicationException)faultDetail.getCause()));
} else {
return new InvocationResponseImpl(null, new CougarClientException(code, faultDetail.getDetailMessage(), faultDetail.getCause()));
}
}
else {
FaultCode faultCode = code == ServerFaultCode.ServiceCheckedException ? FaultCode.Server : code.getResponseCode().getFaultCode();
return new InvocationResponseImpl(null, new CougarClientException(code, faultCode + " fault received from remote server: "+code,
new CougarClientException(code, faultDetail.getDetailMessage())
));
}
}
else {
return new InvocationResponseImpl(null, new CougarClientException(code, "No detailed message available"));
}
}
}
catch (ClassNotFoundException e) {
throw new TranscriptionException(e);
}
catch (IOException e) {
throw new TranscriptionException(e);
}
}
private void writeOperationKey(OperationKey operationKey, CougarObjectOutput out) throws IOException {
out.writeInt(operationKey.getVersion().getMajor());
out.writeInt(operationKey.getVersion().getMinor());
out.writeString(operationKey.getServiceName());
out.writeString(operationKey.getOperationName());
}
public OperationKey readOperationKey(CougarObjectInput in) throws IOException {
EnumUtils.setHardFailureForThisThread(hardFailEnumDeserialisation);
return new OperationKey(
new ServiceVersion(in.readInt(), in.readInt()),
in.readString(),
in.readString());
}
void writeArgs(Parameter [] argTypes, Object [] args, CougarObjectOutput out) throws IOException {
out.writeInt(argTypes.length);
for (int i=0;i<argTypes.length; i++) {
out.writeString(argTypes[i].getName());
}
out.writeObject(args);
}
void writeTimeConstraints(TimeConstraints timeConstraints, CougarObjectOutput out, byte protocolVersion) throws IOException {
if (protocolVersion >= CougarProtocol.TRANSPORT_PROTOCOL_VERSION_TIME_CONSTRAINTS) {
boolean haveTimeConstraints = timeConstraints.getTimeRemaining() != null;
out.writeBoolean(haveTimeConstraints);
if (haveTimeConstraints) {
out.writeLong(timeConstraints.getTimeRemaining());
}
}
}
public Object [] readArgs(Parameter [] argTypes, CougarObjectInput in) throws IOException {
EnumUtils.setHardFailureForThisThread(hardFailEnumDeserialisation);
try {
int numArgs = in.readInt();
String[] paramNames = new String[numArgs];
for (int i=0;i<numArgs;i++) {
paramNames[i] = in.readString();
}
return ArgumentMatcher.getArgumentValues(argTypes, paramNames, (Object[])in.readObject());
}
catch (IOException e) {
throw new TranscriptionException(e);
}
catch (ClassNotFoundException e) {
throw new TranscriptionException(e);
}
}
private void writeExecutionContext(ExecutionContext ctx, CougarObjectOutput out, IdentityResolver identityResolver, byte protocolVersion) throws IOException {
writeGeoLocation(ctx.getLocation(), out, protocolVersion);
writeIdentity(ctx.getIdentity(), out, identityResolver);
writeRequestUUID(ctx.getRequestUUID(), out);
writeReceivedTime(ctx.getReceivedTime(), out);
out.writeBoolean(ctx.traceLoggingEnabled());
writeRequestTime(out, protocolVersion);
}
void writeRequestTime(CougarObjectOutput out, byte protocolVersion) throws IOException {
if (protocolVersion >= CougarProtocol.TRANSPORT_PROTOCOL_VERSION_TIME_CONSTRAINTS) {
out.writeLong(System.currentTimeMillis());
}
}
private ExecutionContextWithTokens resolveExecutionContext(CougarObjectInput in, GeoLocationDetails geo, List<IdentityToken> tokens, int transportSecurityStrengthFactor, byte protocolVersion) throws IOException {
final RequestUUID uuid = readRequestUUID(in);
final Date receivedTime = readReceivedTime(in);
final boolean traceEnabled = in.readBoolean();
final Long requestTime = protocolVersion >= CougarProtocol.TRANSPORT_PROTOCOL_VERSION_TIME_CONSTRAINTS ? in.readLong() : System.currentTimeMillis();
Date requestDate = requestTimeResolver.resolveRequestTime(requestTime);
return ExecutionContextFactory.resolveExecutionContext(tokens, uuid, geo, receivedTime, traceEnabled, transportSecurityStrengthFactor, requestDate, false);
}
@Override
public ExecutionContextWithTokens readExecutionContext(CougarObjectInput in, String remoteAddress, X509Certificate[] clientCertChain, int transportSecurityStrengthFactor, byte protocolVersion) throws IOException {
EnumUtils.setHardFailureForThisThread(hardFailEnumDeserialisation);
// this has to be first as the protocol requires it
final GeoLocationDetails geo = readGeoLocation(in, remoteAddress, protocolVersion);
List<IdentityToken> tokens = identityTokenResolver.resolve(in, clientCertChain);
return resolveExecutionContext(in, geo, tokens, transportSecurityStrengthFactor, protocolVersion);
}
@Override
public TimeConstraints readTimeConstraintsIfPresent(CougarObjectInput in, byte protocolVersion) throws IOException {
if (protocolVersion >= CougarProtocol.TRANSPORT_PROTOCOL_VERSION_TIME_CONSTRAINTS) {
boolean haveTimeConstraints = in.readBoolean();
if (haveTimeConstraints) {
long timeout = in.readLong();
// this is the 'raw' time constraint which will be combined with the resolved client request time in the command resolver
return DefaultTimeConstraints.fromTimeout(timeout);
}
}
return DefaultTimeConstraints.NO_CONSTRAINTS;
}
void writeIdentity(IdentityChain identity, CougarObjectOutput out, IdentityResolver identityResolver) throws IOException {
List<IdentityToken> identityTokens = null;
if (identityResolver != null) {
identityTokens = identityResolver.tokenise(identity);
}
identityTokenResolver.rewrite(identityTokens, out);
}
GeoLocationDetails readGeoLocation(CougarObjectInput in, String remoteAddress, byte protocolVersion) throws IOException {
// The current implementation is analogous to an http connection, so the IP
// must be provided, but everything else is created on the server. This might
// become a problem if non-IP based clients are required, but there are no
// requirements for them currently so to guess how they will work is pointless.
String resolvedAddresses = in.readString();
List<String> addressList = RemoteAddressUtils.parse(null, resolvedAddresses);
String inferredCountry = null;
if (protocolVersion >= CougarProtocol.TRANSPORT_PROTOCOL_VERSION_START_TLS) {
inferredCountry = in.readString();
}
// no remote address for socket protocol.
return geoIpLocator.getGeoLocation(remoteAddress, addressList, inferredCountry);
}
void writeGeoLocation(GeoLocationDetails geo, CougarObjectOutput out, byte protocolVersion) throws IOException {
// See comment about reading geo location.
String resolvedAddresses = RemoteAddressUtils.externaliseWithLocalAddresses(geo.getResolvedAddresses());
out.writeString(resolvedAddresses);
if (protocolVersion >= CougarProtocol.TRANSPORT_PROTOCOL_VERSION_START_TLS) {
out.writeString(geo.getInferredCountry());
}
}
private RequestUUID readRequestUUID(CougarObjectInput in) throws IOException {
if (in.readBoolean()) {
String uuidString = in.readString();
return new RequestUUIDImpl(uuidString);
} else {
return new RequestUUIDImpl();
}
}
void writeRequestUUID(RequestUUID uuid, CougarObjectOutput out) throws IOException {
if (uuid != null) {
out.writeBoolean(true);
out.writeString(uuid.toString());
} else {
out.writeBoolean(false);
}
}
private Date readReceivedTime(CougarObjectInput in) throws IOException {
if (in.readBoolean()) {
Long ticks = in.readLong();
return new Date(ticks);
}
return null;
}
void writeReceivedTime(Date receivedTime, CougarObjectOutput out) throws IOException {
if (receivedTime != null) {
out.writeBoolean(true);
out.writeLong(receivedTime.getTime());
} else {
out.writeBoolean(false);
}
}
@ManagedAttribute
public boolean isHardFailEnumDeserialisation() {
return hardFailEnumDeserialisation;
}
public void setHardFailEnumDeserialisation(boolean hardFailEnumDeserialisation) {
this.hardFailEnumDeserialisation = hardFailEnumDeserialisation;
}
}