/*************************************************************************
* Copyright 2009-2014 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.simpleworkflow.ws;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nonnull;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import com.eucalyptus.auth.InvalidAccessKeyAuthException;
import com.eucalyptus.auth.InvalidSignatureAuthException;
import com.eucalyptus.binding.BindingException;
import com.eucalyptus.context.Context;
import com.eucalyptus.context.Contexts;
import com.eucalyptus.context.NoSuchContextException;
import com.eucalyptus.crypto.util.Timestamps;
import com.eucalyptus.http.MappingHttpRequest;
import com.eucalyptus.http.MappingHttpResponse;
import com.eucalyptus.simpleworkflow.SwfJsonUtils;
import com.eucalyptus.simpleworkflow.common.model.SimpleWorkflowMessage;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.UnsafeByteArrayOutputStream;
import com.eucalyptus.ws.handlers.ExceptionMarshallerHandler;
import com.eucalyptus.ws.handlers.MessageStackHandler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import edu.ucsb.eucalyptus.msgs.BaseMessage;
import edu.ucsb.eucalyptus.msgs.EucalyptusErrorMessageType;
import edu.ucsb.eucalyptus.msgs.ExceptionResponseType;
/**
*
*/
public class SimpleWorkflowBinding extends MessageStackHandler implements ExceptionMarshallerHandler {
@Override
public void incomingMessage( final ChannelHandlerContext ctx, final MessageEvent event ) throws Exception {
if ( event.getMessage( ) instanceof MappingHttpRequest ) {
MappingHttpRequest httpRequest = ( MappingHttpRequest ) event.getMessage( );
try {
BaseMessage msg = bind( httpRequest );
httpRequest.setMessage( msg );
} catch ( Exception e ) {
if ( !( e instanceof BindingException ) ) {
e = new BindingException( e );
}
throw e;
}
}
}
@Override
public void outgoingMessage( final ChannelHandlerContext ctx, final MessageEvent event ) throws Exception {
if ( event.getMessage( ) instanceof MappingHttpResponse ) {
MappingHttpResponse httpResponse = ( MappingHttpResponse ) event.getMessage( );
UnsafeByteArrayOutputStream byteOut = new UnsafeByteArrayOutputStream( 8192 );
if ( httpResponse.getMessage( ) instanceof EucalyptusErrorMessageType ) {
httpResponse.setStatus( HttpResponseStatus.BAD_REQUEST );
final Optional<String> correlationId = getCorrelationId( event );
if ( correlationId.isPresent( ) ) httpResponse.addHeader( "x-amzn-RequestId", correlationId.get( ) );
SwfJsonUtils.writeObject( byteOut, ImmutableMap.of(
"__type", "InternalFailure",
"message", ((EucalyptusErrorMessageType)httpResponse.getMessage( )).getMessage() ) );
} else if ( httpResponse.getMessage( ) instanceof ExceptionResponseType ) { //handle error case specially
ExceptionResponseType msg = ( ExceptionResponseType ) httpResponse.getMessage( );
httpResponse.setStatus( msg.getHttpStatus( ) );
final Optional<String> correlationId = getCorrelationId( event );
if ( correlationId.isPresent( ) ) httpResponse.addHeader( "x-amzn-RequestId", correlationId.get( ) );
SwfJsonUtils.writeObject( byteOut, ImmutableMap.of(
"__type", "InternalFailure",
"message", ((ExceptionResponseType)httpResponse.getMessage( )).getMessage() ) );
} else if ( httpResponse.getMessage( ) != null ){ //actually try to bind response
final Object message = httpResponse.getMessage( );
if ( message instanceof SimpleWorkflowMessage ) {
httpResponse.addHeader( "x-amzn-RequestId", ( (SimpleWorkflowMessage) message ).getCorrelationId() );
if ( !SimpleWorkflowMessage.class.equals( message.getClass() ) ) {
SwfJsonUtils.writeObject( byteOut, message );
}
}
}
ChannelBuffer buffer = ChannelBuffers.wrappedBuffer( byteOut.getBuffer( ), 0, byteOut.getCount( ) );
httpResponse.addHeader( HttpHeaders.Names.CONTENT_LENGTH, String.valueOf( buffer.readableBytes( ) ) );
httpResponse.addHeader( HttpHeaders.Names.CONTENT_TYPE, "application/x-amz-json-1.0" );
httpResponse.setContent( buffer );
}
}
@Nonnull
@Override
public ExceptionResponse marshallException( @Nonnull ChannelEvent event,
@Nonnull final HttpResponseStatus status,
@Nonnull final Throwable throwable ) throws Exception {
HttpResponseStatus responseStatus = status;
final Map<String,String> headers = Maps.newHashMap();
headers.put( HttpHeaders.Names.DATE, Timestamps.formatRfc822Timestamp( new Date( ) ) );
headers.put( HttpHeaders.Names.CONTENT_TYPE, "application/x-amz-json-1.0" );
final Optional<String> correlationId = getCorrelationId( event );
if ( correlationId.isPresent( ) ) {
headers.put( "x-amzn-RequestId", correlationId.get( ) );
}
String type = "InternalFailure"; // code
String message = throwable.getMessage( );
if ( Exceptions.isCausedBy( throwable, InvalidAccessKeyAuthException.class ) ) {
responseStatus = HttpResponseStatus.FORBIDDEN;
type = "InvalidClientTokenId";
message = "The security token included in the request is invalid.";
} else if ( Exceptions.isCausedBy( throwable, InvalidSignatureAuthException.class ) ) {
responseStatus = HttpResponseStatus.FORBIDDEN;
type = "InvalidSignatureException";
message = "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method.";
} else if ( throwable instanceof BindingException ) {
responseStatus = HttpResponseStatus.BAD_REQUEST;
type = "InvalidParameterValue";
}
final UnsafeByteArrayOutputStream byteOut = new UnsafeByteArrayOutputStream( 8192 );
SwfJsonUtils.writeObject( byteOut, ImmutableMap.of( "__type", type, "message", message ) );
return new ExceptionResponse(
responseStatus,
ChannelBuffers.wrappedBuffer( byteOut.getBuffer( ), 0, byteOut.getCount( ) ),
ImmutableMap.copyOf( headers )
);
}
@SuppressWarnings( "unchecked" )
private BaseMessage bind( final MappingHttpRequest httpRequest ) throws BindingException, IOException {
// find action
final String target = Objects.toString( httpRequest.getHeader( "X-Amz-Target" ), "" );
final String simpleClassName;
if ( target.startsWith( "SimpleWorkflowService." ) ) {
simpleClassName = target.substring( 22 ) + "Request";
} else {
throw new BindingException( "Unable to get action from target header: " + target );
}
// parse message
final String content = httpRequest.getContentAsString( );
final BaseMessage message;
try {
message = SwfJsonUtils.readObject(
content,
(Class<? extends BaseMessage>) Class.forName( SimpleWorkflowMessage.class.getPackage( ).getName( ) + "." + simpleClassName ) );
} catch ( JsonProcessingException e ) {
throw new BindingException( e.getMessage( ) );
} catch ( ClassNotFoundException e ) {
throw new BindingException( "Binding not found for target: " + target );
}
return message;
}
private Optional<String> getCorrelationId( final ChannelEvent event ) {
return getCorrelationId( event.getChannel( ) );
}
private Optional<String> getCorrelationId( final Channel channel ) {
try {
final Context context = Contexts.lookup( channel );
return Optional.fromNullable( context.getCorrelationId( ) );
} catch ( NoSuchContextException e ) {
return Optional.absent( );
}
}
}