/************************************************************************* * Copyright 2009-2013 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. * * This file may incorporate work covered under the following copyright * and permission notice: * * Software License Agreement (BSD License) * * Copyright (c) 2008, Regents of the University of California * All rights reserved. * * Redistribution and use of this software in source and binary forms, * with or without modification, are permitted provided that the * following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE * THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL, * COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE, * AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING * IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA, * SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY, * WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION, * REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO * IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT * NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS. ************************************************************************/ package com.eucalyptus.ws.server; import static com.eucalyptus.auth.principal.TemporaryAccessKey.TemporaryKeyType; import java.lang.reflect.Modifier; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.net.URLCodec; import org.apache.log4j.Logger; import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import com.eucalyptus.bootstrap.Bootstrap; import com.eucalyptus.bootstrap.Bootstrapper; import com.eucalyptus.bootstrap.Provides; import com.eucalyptus.bootstrap.RunDuring; import com.eucalyptus.bootstrap.ServiceJarDiscovery; import com.eucalyptus.component.ComponentId; import com.eucalyptus.component.ComponentIds; import com.eucalyptus.component.annotation.AwsServiceName; import com.eucalyptus.component.annotation.ComponentPart; import com.eucalyptus.component.annotation.PublicService; import com.eucalyptus.empyrean.Empyrean; import com.eucalyptus.http.MappingHttpMessage; import com.eucalyptus.http.MappingHttpRequest; import com.eucalyptus.records.EventRecord; import com.eucalyptus.records.EventType; import com.eucalyptus.records.Logs; import com.eucalyptus.system.Ats; import com.eucalyptus.util.Classes; import com.eucalyptus.ws.Handlers; import com.eucalyptus.ws.handlers.HmacHandler; import com.eucalyptus.ws.handlers.MessageStackHandler; import com.eucalyptus.ws.protocol.BaseQueryBinding; import com.eucalyptus.ws.protocol.OperationParameter; import com.eucalyptus.ws.handlers.SoapHandler; import com.eucalyptus.ws.util.HmacUtils.SignatureVersion; import com.google.common.base.Supplier; import com.google.common.collect.Sets; import edu.ucsb.eucalyptus.cloud.entities.SystemConfiguration; /** * Utilities for discovering pipelines. */ public class Pipelines { private static final Logger LOG = Logger.getLogger( Pipelines.class ); private static final Set<FilteredPipeline> internalPipelines = Sets.newHashSet( ); private static final Set<FilteredPipeline> pipelines = Sets.newHashSet( ); //GRZE:TODO: this is not happy ==> {@link DomainNames} private static final Supplier<String> subDomain = () -> SystemConfiguration.getSystemConfiguration( ).getDnsDomain( ); /** * Finds and returns a pipeline that accepts the {@code request} by checking registered pipelines. * * @throws NoAcceptingPipelineException if no accepting pipeline for the {@code request} can be found */ static FilteredPipeline find( final HttpRequest request ) throws DuplicatePipelineException, NoAcceptingPipelineException { final FilteredPipeline candidate = findAccepting( request ); if ( candidate == null ) { if ( Logs.isExtrrreeeme( ) ) { if ( request instanceof MappingHttpMessage ) { ( ( MappingHttpMessage ) request ).logMessage( ); for ( final FilteredPipeline p : pipelines ) { LOG.debug( "PIPELINE: " + p ); } for ( final FilteredPipeline p : internalPipelines ) { LOG.debug( "PIPELINE: " + p ); } } } throw new NoAcceptingPipelineException( ); } if ( Logs.isExtrrreeeme( ) ) { EventRecord.here( Pipelines.class, EventType.PIPELINE_UNROLL, candidate.toString( ) ).extreme( ); } return candidate; } /** * Finds and returns a pipeline that accepts the {@code request} by checking registered pipelines. * * @return an accepting pipeline else {@code null} */ private static FilteredPipeline findAccepting( final HttpRequest request ) { for ( final FilteredPipeline f : pipelines ) { if ( f.checkAccepts( request ) ) { return f; } } final String hostHeader = request.getHeader( HttpHeaders.Names.HOST ); if ( hostHeader != null && ( hostHeader.contains( "amazonaws.com" ) || hostHeader.contains( subDomain.get( ) ) ) ) { final String host = hostHeader.indexOf( ':' ) > 0 ? hostHeader.substring( 0, hostHeader.indexOf( ':' ) ) : hostHeader; LOG.debug( "Trying to intercept request for " + hostHeader ); for ( final FilteredPipeline f : pipelines ) { if ( Ats.from( f ).has( ComponentPart.class ) ) { Class<? extends ComponentId> compIdClass = Ats.from( f ).get( ComponentPart.class ).value( ); ComponentId compId = ComponentIds.lookup( compIdClass ); if ( Ats.from( compIdClass ).has( PublicService.class ) ) { if ( request.getHeaderNames().contains( "SOAPAction" ) && f.addHandlers( Channels.pipeline( ) ).get( SoapHandler.class ) == null ) { continue;//Skip pipeline which doesn't handle SOAP for this SOAP request } else if ( !request.getHeaderNames().contains( "SOAPAction" ) && f.addHandlers( Channels.pipeline( ) ).get( SoapHandler.class ) != null ) { continue;//Skip pipeline which handles SOAP for this non-SOAP request } LOG.debug( "Maybe intercepting: " + hostHeader + " using " + f.getClass( ) ); if ( Ats.from( compIdClass ).has( AwsServiceName.class ) && host.matches( "[\\w\\.-_]*" + compId.getAwsServiceName( ) + "(?:\\.[\\w\\-]+)?\\.amazonaws.com" ) ) { return f;//Return pipeline which can handle the request for ${service}.${region}.amazonaws.com } else if ( host.matches( "[\\w\\.-_]*" + compId.name( ) + "\\." + subDomain.get( ) ) ) { return f;//Return pipeline which can handle the request for ${service}.${system.dns.dnsdomain} } } } } } for ( final FilteredPipeline f : internalPipelines ) { if ( f.checkAccepts( request ) ) { return f; } } return null; } /** * Registers internal query and SOAP pipelines for all components. */ @Provides( Empyrean.class ) @RunDuring( Bootstrap.Stage.UnprivilegedConfiguration ) public static class PipelineBootstrapper extends Bootstrapper.Simple { @Override public boolean load( ) throws Exception { for ( final ComponentId comp : ComponentIds.list( ) ) { Pipelines.internalPipelines.add( new InternalQueryPipeline( comp ) ); Pipelines.internalPipelines.add( new InternalSoapPipeline( comp ) ); } return true; } } /** * Discovers and registers FilteredPipeline instances for discovered components. */ public static class PipelineDiscovery extends ServiceJarDiscovery { @SuppressWarnings( { "rawtypes", "unchecked", "synthetic-access" } ) @Override public boolean processClass( final Class candidate ) throws Exception { if ( FilteredPipeline.class.isAssignableFrom( candidate ) && !Modifier.isAbstract( candidate.getModifiers( ) ) && !Modifier.isInterface( candidate.getModifiers( ) ) && Ats.from( candidate ).has( ComponentPart.class ) ) { try { final ComponentId compId = Ats.from( candidate ).get( ComponentPart.class ).value( ).newInstance( ); final Class<? extends FilteredPipeline> pipelineClass = candidate; final FilteredPipeline pipeline = Classes.newInstance( pipelineClass ); Pipelines.pipelines.add( pipeline ); return true; } catch ( final Exception ex ) { LOG.trace( ex, ex ); return false; } } else { return false; } } @Override public Double getPriority( ) { return 0.1d; } } private static class InternalSoapPipeline extends FilteredPipeline.InternalPipeline { private final String servicePath; private final String internalServicePath; private final String serviceName; public InternalSoapPipeline( final ComponentId componentId ) { super( componentId ); this.servicePath = componentId.getServicePath( ); this.internalServicePath = componentId.getInternalServicePath( ); this.serviceName = componentId.getFullName( ).toString( ); } @Override public boolean checkAccepts( final HttpRequest message ) { return ( message.getUri( ).endsWith( this.servicePath ) || message.getUri( ).endsWith( this.internalServicePath ) ) && message.getHeaderNames( ).contains( "SOAPAction" ); } @Override public String getName( ) { return "internal-soap-pipeline-" + this.serviceName.toLowerCase( ) + "-" + this.servicePath; } @Override public ChannelPipeline addHandlers( final ChannelPipeline pipeline ) { pipeline.addLast( "deserialize", Handlers.soapMarshalling( ) ); pipeline.addLast( "security-detection-handler", new MessageStackHandler( ) { @Override public void incomingMessage( final ChannelHandlerContext ctx, final MessageEvent event ) throws Exception { boolean addWsSecurity = true; if ( event.getMessage( ) instanceof MappingHttpRequest ) { final MappingHttpRequest request = (MappingHttpRequest) event.getMessage( ); addWsSecurity = !request.containsHeader( HttpHeaders.Names.AUTHORIZATION ); } if ( addWsSecurity ) { ctx.getPipeline( ).addAfter( ctx.getName( ), "ws-security", Handlers.internalWsSecHandler( ) ); } else { ctx.getPipeline( ).addAfter( ctx.getName( ), "timestamp-verify", Handlers.queryTimestamphandler() ); ctx.getPipeline( ).addAfter( ctx.getName( ), "hmac-verify", new HmacHandler( EnumSet.of(TemporaryKeyType.Session,TemporaryKeyType.Access), EnumSet.of(SignatureVersion.SignatureV4) ) ); } ctx.getPipeline( ).remove( this ); } @Override public void outgoingMessage( final ChannelHandlerContext ctx, final MessageEvent event ) throws Exception { ctx.getPipeline( ).addBefore( ctx.getName( ), "ws-security", Handlers.internalWsSecHandler( ) ); ctx.getPipeline( ).remove( this ); } } ); pipeline.addLast( "ws-addressing", Handlers.addressingHandler( ) ); pipeline.addLast( "build-soap-envelope", Handlers.soapHandler( ) ); pipeline.addLast( "binding", Handlers.bindingHandler( ) ); return pipeline; } @Override public String toString( ) { return String.format( "InternalSoapPipeline:servicePath=%s:serviceName=%s:toString()=%s", this.servicePath, this.serviceName, super.toString( ) ); } } private static class InternalQueryPipeline extends FilteredPipeline.InternalPipeline { public enum RequiredQueryParams { Version } private final String servicePath; private final String internalServicePath; private final String serviceName; public InternalQueryPipeline( final ComponentId componentId ) { super( componentId ); this.servicePath = componentId.getServicePath( ); this.internalServicePath = componentId.getInternalServicePath( ); this.serviceName = componentId.getFullName( ).toString( ); } @Override public boolean checkAccepts( final HttpRequest message ) { if ( message instanceof MappingHttpRequest ) { final MappingHttpRequest httpRequest = ( MappingHttpRequest ) message; final boolean noPath = message.getUri( ).isEmpty( ) || message.getUri( ).equals( "/" ) || message.getUri( ).startsWith( "/?" ); if ( !( message.getUri( ).startsWith( this.servicePath ) || message.getUri( ).startsWith( this.internalServicePath ) ) && !(noPath && resolvesByHost( message.getHeader( HttpHeaders.Names.HOST ) ) ) ) { return false; } if ( httpRequest.getMethod( ).equals( HttpMethod.POST ) && !message.getHeaderNames().contains( "SOAPAction" ) ) { final Map<String, String> parameters = new HashMap<>( httpRequest.getParameters( ) ); final Set<String> nonQueryParameters = Sets.newHashSet( ); final String query = httpRequest.getContentAsString( ); for ( final String p : query.split( "&" ) ) { final String[] splitParam = p.split( "=" ); String lhs = splitParam[0]; String rhs = splitParam.length == 2 ? splitParam[1] : null; try { if ( lhs != null ) lhs = new URLCodec( ).decode( lhs ); } catch ( final DecoderException e ) {} try { if ( rhs != null ) rhs = new URLCodec( ).decode( rhs ); } catch ( final DecoderException e ) {} parameters.put( lhs, rhs ); nonQueryParameters.add( lhs ); } for ( final RequiredQueryParams p : RequiredQueryParams.values( ) ) { if ( !parameters.containsKey( p.toString( ) ) ) { return false; } } httpRequest.getParameters( ).putAll( parameters ); httpRequest.addNonQueryParameterKeys( nonQueryParameters ); } else { for ( final RequiredQueryParams p : RequiredQueryParams.values( ) ) { if ( !httpRequest.getParameters( ).containsKey( p.toString( ) ) ) { return false; } } } return true; } return false; } @Override public String getName( ) { return "internal-query-pipeline-" + this.serviceName.toLowerCase( ) + "-" + this.servicePath; } @Override public ChannelPipeline addHandlers( final ChannelPipeline pipeline ) { pipeline.addLast( "hmac-verify", new HmacHandler( EnumSet.of(TemporaryKeyType.Role, TemporaryKeyType.Access, TemporaryKeyType.Session) ) ); pipeline.addLast( "timestamp-verify", Handlers.queryTimestamphandler() ); pipeline.addLast( "restful-binding", new InternalQueryBinding( ) ); return pipeline; } @Override public String toString( ) { return String.format( "InternalQueryPipeline:servicePath=%s:serviceName=%s:toString()=%s", this.servicePath, this.serviceName, super.toString( ) ); } } static class InternalQueryBinding extends BaseQueryBinding<OperationParameter> implements ChannelHandler { public InternalQueryBinding( ) { super( "http://ec2.amazonaws.com/doc/%s/", "2009-04-04", OperationParameter.Action, OperationParameter.Operation ); } } }