/* * 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.objectstorage.client; import java.net.URI; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; import org.apache.log4j.Logger; import com.amazonaws.ClientConfiguration; import com.amazonaws.Protocol; import com.amazonaws.SDKGlobalConfiguration; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.http.AmazonHttpClient; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.S3ClientOptions; import com.eucalyptus.auth.AuthException; import com.eucalyptus.auth.principal.AccessKey; import com.eucalyptus.auth.principal.User; import com.eucalyptus.component.ServiceConfiguration; import com.eucalyptus.component.ServiceUris; import com.eucalyptus.component.Topology; import com.eucalyptus.crypto.Crypto; import com.eucalyptus.objectstorage.ObjectStorage; import com.eucalyptus.util.Pair; import com.eucalyptus.ws.StackConfiguration; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; /** * A convenience wrapper for an AWS Java SDK S3 Client that sets default timeouts etc, options, etc for operating against the ObjectStorage service in * Eucalyptus. All internal system uses of ObjectStorage should use this or an extension of it rather than internal dispatch mechanisms. */ public class GenericS3ClientFactory { private static final Logger LOG = Logger.getLogger(GenericS3ClientFactory.class); private static final Random randomizer = new Random(System.currentTimeMillis()); static { System.setProperty(SDKGlobalConfiguration.DISABLE_REMOTE_REGIONS_FILE_SYSTEM_PROPERTY, "disable"); // anything non-null disables it System.setProperty(SDKGlobalConfiguration.DEFAULT_S3_STREAM_BUFFER_SIZE, String.valueOf(GenericS3ClientFactoryConfiguration.getInstance().getBuffer_size())); // 512KB upload buffer, to handle most small objects System.setProperty("com.amazonaws.services.s3.disableGetObjectMD5Validation", "disable"); // disable etag validation on GETs } /** * A credentials provider that will get the first active access key for the given user. A refresh may use the same credentials or new ones if the * set of active keys has changed */ public static class EucaUserCredentialsProvider implements AWSCredentialsProvider { private User eucaUser; private AWSCredentials currentCredential; public EucaUserCredentialsProvider(User user) { this.eucaUser = user; } @Override public synchronized AWSCredentials getCredentials() { if (currentCredential == null) { updateCreds(); } return currentCredential; } protected void updateCreds() { try { AccessKey userAccessKey = Iterables.find(eucaUser.getKeys(), new Predicate<AccessKey>() { @Override public boolean apply(@Nullable AccessKey accessKey) { return accessKey != null && accessKey.isActive(); } }); currentCredential = new BasicAWSCredentials(userAccessKey.getAccessKey(), userAccessKey.getSecretKey()); } catch (AuthException e) { throw new RuntimeException("No active credentials for the user"); } } @Override public synchronized void refresh() { updateCreds(); } } public static AWSCredentialsProvider getEucaUserAWSCredentialsProvider(User user) { return new EucaUserCredentialsProvider(user); } /** * Uses the first key found for the given user to construct an s3 client * * @param clientUser * @param useHttps * @return * @throws AuthException if user has no access keys active * @throws java.util.NoSuchElementException if no OSG found ENABLED */ protected static AmazonS3Client getS3ClientForUser(final User clientUser, boolean useHttps) throws AuthException, NoSuchElementException { try { return getS3Client(getEucaUserAWSCredentialsProvider(clientUser), useHttps); } catch (Exception e) { LOG.error("Could not generate s3 client for user " + clientUser.getUserId() + " because no active access keys found.", e); throw new AuthException("No active access keys found for user", e); } } protected static EucaHttpClientKey buildHttpClientKey( boolean withHttps ) { final GenericS3ClientFactoryConfiguration s3ClientFactoryConfiguration = GenericS3ClientFactoryConfiguration.getInstance( ); return new EucaHttpClientKey( s3ClientFactoryConfiguration.getConnection_timeout_ms(), s3ClientFactoryConfiguration.getSocket_read_timeout_ms(), withHttps, s3ClientFactoryConfiguration.getSigner_type(), s3ClientFactoryConfiguration.getMax_connections(), s3ClientFactoryConfiguration.getMax_error_retries() ); } public static S3ClientOptions getDefaultClientOptions() { S3ClientOptions ops = new S3ClientOptions(); ops.setPathStyleAccess(true); return ops; } public static AmazonS3Client getS3Client(AWSCredentialsProvider provider, boolean https) throws NoSuchElementException { EucaHttpClientKey config = buildHttpClientKey(https); AmazonS3Client s3Client = new EucaS3Client(provider, config); s3Client.setS3ClientOptions(getDefaultClientOptions()); s3Client.setEndpoint(getRandomOSGUri().toString()); return s3Client; } /** * Returns a configured S3 client for the specified set of credentials. * * @param credentials * @param https * @return * @throws NoSuchElementException if no ENABLED OSG found */ public static AmazonS3Client getS3Client(AWSCredentials credentials, boolean https) throws NoSuchElementException { return getS3Client( new AWSStaticCredentialsProvider( credentials ), https ); } protected static URI getRandomOSGUri(boolean usePublicDns) throws NoSuchElementException { List<ServiceConfiguration> osgs = Lists.newArrayList(Topology.lookupMany(ObjectStorage.class)); if (osgs == null || osgs.size() == 0) { throw new NoSuchElementException("No ENABLED OSGs found. Cannot generate client with no set endpoint"); } else { int osgIndex = randomizer.nextInt(osgs.size()); LOG.trace("Using osg index " + osgIndex + " from list: " + osgs); ServiceConfiguration conf = osgs.get(osgIndex); if (usePublicDns) { return ServiceUris.remotePublicify(conf); } else { return ServiceUris.remote(conf); } } } protected static URI getRandomOSGUri() throws NoSuchElementException { return getRandomOSGUri(false); } /** * Extension of AmazonS3Client to use a shared HTTP client. */ private static final class EucaS3Client extends AmazonS3Client { private static final AtomicReference<Pair<EucaHttpClientKey,EucaHttpClient>> clientPairRef = new AtomicReference<>( ); EucaS3Client( final AWSCredentialsProvider credentialsProvider, final EucaHttpClientKey key ) { super( credentialsProvider, key.toClientConfiguration( ) ); this.client.shutdown( ); final Pair<EucaHttpClientKey,EucaHttpClient> clientPair = clientPairRef.get( ); if ( clientPair != null && clientPair.getLeft( ).equals( key ) ) { this.client = clientPair.getRight( ).ref( ); } else { final EucaHttpClient eucaClient = new EucaHttpClient( clientConfiguration ).ref( ); if ( clientPairRef.compareAndSet( clientPair, Pair.of( key, eucaClient ) ) ) { // unref/ref for atomic reference eucaClient.ref( ); if ( clientPair != null ) { clientPair.getRight( ).unref( ); } } this.client = eucaClient; } } } private static final class EucaHttpClientKey { private final int connectionTimeout; private final int socketTimeout; private final boolean https; private final String signerOverride; private final int maxConnections; private final int maxErrorRetries; private final long periodId; // so key identity changes periodically triggering replacement EucaHttpClientKey( final int connectionTimeout, final int socketTimeout, final boolean https, final String signerOverride, final int maxConnections, final int maxErrorRetries ) { this.connectionTimeout = connectionTimeout; this.socketTimeout = socketTimeout; this.https = https; this.signerOverride = signerOverride; this.maxConnections = maxConnections; this.maxErrorRetries = maxErrorRetries; this.periodId = System.currentTimeMillis( ) / TimeUnit.HOURS.toMillis( 1 ); } /** * Convert key to client configuration. * * Values must be constant or come from the key and be part of identity */ ClientConfiguration toClientConfiguration( ) { final ClientConfiguration config = new ClientConfiguration( ); config.setConnectionTimeout( connectionTimeout ); config.setSocketTimeout( socketTimeout ); config.setProtocol( https ? Protocol.HTTPS : Protocol.HTTP ); config.setSignerOverride( signerOverride ); config.setMaxConnections( maxConnections ); config.setMaxErrorRetry( maxErrorRetries ); config.setUseReaper( true ); config.setUseThrottleRetries( false ); config.setCacheResponseMetadata( false ); config.setConnectionMaxIdleMillis( 45_000 ); config.setSecureRandom( Crypto.getSecureRandomSupplier( ).get( ) ); return config; } @Override public boolean equals( final Object o ) { if ( this == o ) return true; if ( o == null || getClass( ) != o.getClass( ) ) return false; final EucaHttpClientKey that = (EucaHttpClientKey) o; return connectionTimeout == that.connectionTimeout && socketTimeout == that.socketTimeout && https == that.https && maxConnections == that.maxConnections && maxErrorRetries == that.maxErrorRetries && Objects.equals( signerOverride, that.signerOverride ) && periodId == that.periodId; } @Override public int hashCode() { return Objects.hash( connectionTimeout, socketTimeout, https, signerOverride, maxConnections, maxErrorRetries, periodId ); } } /** * Extension of AmazonHttpClient with reference tracking */ private static final class EucaHttpClient extends AmazonHttpClient { private final AtomicInteger refs = new AtomicInteger( 0 ); EucaHttpClient( final ClientConfiguration configuration ) { super( configuration, null, true, false ); } public EucaHttpClient ref( ) { refs.incrementAndGet( ); return this; } public EucaHttpClient unref( ) { if ( refs.decrementAndGet( ) <= 0 ) { super.shutdown( ); } return this; } @Override public void shutdown( ) { unref( ); } } }