/************************************************************************* * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP * * 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/. ************************************************************************/ package com.eucalyptus.ws.protocol; import static com.eucalyptus.util.Json.JsonOption.IgnoreBaseMessage; import static com.eucalyptus.util.Json.JsonOption.IgnoreGroovy; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.EnumSet; 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.binding.BindingManager; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.context.NoSuchContextException; import com.eucalyptus.http.MappingHttpResponse; import com.eucalyptus.util.Exceptions; import com.eucalyptus.util.Json; import com.eucalyptus.util.UnsafeByteArrayOutputStream; import com.eucalyptus.ws.handlers.ExceptionMarshallerHandler; import com.fasterxml.jackson.databind.ObjectWriter; 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; import javaslang.control.Option; /** * */ public class BaseQueryJsonBinding<T extends Enum<T>> extends BaseQueryBinding<T> implements ExceptionMarshallerHandler { private static final String DEFAULT_RESPONSE_CONTENT_TYPE = "application/x-amz-json-1.0"; private static final String CODE_ERROR = "InternalFailure"; private static final String HEADER_REQUEST_ID = "x-amzn-RequestId"; private static final ObjectWriter writer = Json.mapper( EnumSet.of( IgnoreBaseMessage, IgnoreGroovy ) ).writer( ); private final Class<? extends BaseMessage> messageType; private final String responseContentType; @SuppressWarnings( "unchecked" ) public BaseQueryJsonBinding( final Class<? extends BaseMessage> messageType, final Option<String> responseContentType, final UnknownParameterStrategy unknownParameterStrategy, final T operationParam, final T... alternativeOperationParam ) { super( BindingManager.defaultBindingNamespace( ), null, unknownParameterStrategy, operationParam, alternativeOperationParam ); this.messageType = messageType; this.responseContentType = responseContentType.getOrElse( DEFAULT_RESPONSE_CONTENT_TYPE ); } @Override public void outgoingMessage( final ChannelHandlerContext ctx, final MessageEvent event ) throws Exception { if ( event.getMessage( ) instanceof MappingHttpResponse ) { Option<String> correlationId = getCorrelationId( event ); MappingHttpResponse httpResponse = ( MappingHttpResponse ) event.getMessage( ); UnsafeByteArrayOutputStream byteOut = new UnsafeByteArrayOutputStream( 8192 ); if ( httpResponse.getMessage( ) instanceof EucalyptusErrorMessageType ) { httpResponse.setStatus( HttpResponseStatus.BAD_REQUEST ); writer.writeValue( byteOut, ImmutableMap.of( "__type", CODE_ERROR, "message", ((EucalyptusErrorMessageType)httpResponse.getMessage( )).getMessage() ) ); } else if ( httpResponse.getMessage( ) instanceof ExceptionResponseType ) { //handle error case specially ExceptionResponseType msg = ( ExceptionResponseType ) httpResponse.getMessage( ); httpResponse.setStatus( msg.getHttpStatus( ) ); writer.writeValue( byteOut, ImmutableMap.of( "__type", CODE_ERROR, "message", ((ExceptionResponseType)httpResponse.getMessage( )).getMessage() ) ); } else if ( httpResponse.getMessage( ) != null ){ //actually try to bind response final Object message = httpResponse.getMessage( ); if ( shouldWrite( message ) ) { correlationId = Option.some( ( (BaseMessage) message ).getCorrelationId( ) ); writeMessage( byteOut, (BaseMessage) 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, responseContentType ); if ( correlationId.isDefined( ) ) { httpResponse.addHeader( HEADER_REQUEST_ID, correlationId.get( ) ); } httpResponse.setContent( buffer ); } } @Nonnull @Override public ExceptionResponse marshallException( @Nonnull ChannelEvent event, @Nonnull final HttpResponseStatus status, @Nonnull final Throwable throwable ) throws Exception { final Map<String,String> headers = Maps.newHashMap(); headers.put( HttpHeaders.Names.CONTENT_TYPE, responseContentType ); final Option<String> correlationId = getCorrelationId( event ); if ( correlationId.isDefined( ) ) { headers.put( HEADER_REQUEST_ID, correlationId.get( ) ); } final ExceptionResponseDetail details = getExceptionResponseDetail( throwable ).getOrElse( ExceptionResponseDetail.of( status, CODE_ERROR, Objects.toString( throwable.getMessage( ), "" ) ) ); final UnsafeByteArrayOutputStream byteOut = new UnsafeByteArrayOutputStream( 8192 ); writer.writeValue( byteOut, ImmutableMap.of( "__type", details.getType( ), "message", details.getMessage( ) ) ); return new ExceptionResponse( details.getStatus( ), ChannelBuffers.wrappedBuffer( byteOut.getBuffer( ), 0, byteOut.getCount( ) ), ImmutableMap.copyOf( headers ) ); } public String marshall( final BaseMessage message ) throws IOException { final UnsafeByteArrayOutputStream byteOut = new UnsafeByteArrayOutputStream( 4096 ); if ( shouldWrite( message ) ) { writer.writeValue( byteOut, message ); } return new String( byteOut.getBuffer( ), 0, byteOut.getCount(), StandardCharsets.UTF_8 ); } protected final boolean shouldWrite( final Object message ) { return messageType.isInstance( message ); } protected final void writeMessage( final OutputStream out, final BaseMessage message ) throws IOException { writer.writeValue( out, message ); } protected final Option<ExceptionResponseDetail> details( HttpResponseStatus status, String type, String message ) { return Option.of( ExceptionResponseDetail.of( status, type, message ) ); } protected Option<ExceptionResponseDetail> getExceptionResponseDetail( final Throwable throwable ) { Option<ExceptionResponseDetail> details = Option.none( ); if ( Exceptions.isCausedBy( throwable, InvalidAccessKeyAuthException.class ) ) { details = details( HttpResponseStatus.FORBIDDEN, "InvalidClientTokenId", "The security token included in the request is invalid." ); } else if ( Exceptions.isCausedBy( throwable, InvalidSignatureAuthException.class ) ) { details = details( HttpResponseStatus.FORBIDDEN, "InvalidSignatureException", "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 ) { details = details( HttpResponseStatus.BAD_REQUEST, "InvalidParameterValue", Objects.toString( throwable.getMessage( ), "" ) ); } return details; } private Option<String> getCorrelationId( final ChannelEvent event ) { return getCorrelationId( event.getChannel( ) ); } private Option<String> getCorrelationId( final Channel channel ) { try { final Context context = Contexts.lookup( channel ); return Option.of( context.getCorrelationId( ) ); } catch ( NoSuchContextException e ) { return Option.none( ); } } public static final class ExceptionResponseDetail { private final HttpResponseStatus status; private final String type; private final String message; private ExceptionResponseDetail( final HttpResponseStatus status, final String type, final String message ) { this.status = status; this.type = type; this.message = message; } public static ExceptionResponseDetail of( final HttpResponseStatus status, final String type, final String message ) { return new ExceptionResponseDetail( status, type, message ); } public HttpResponseStatus getStatus( ) { return status; } public String getType( ) { return type; } public String getMessage( ) { return message; } } }