| /* | |
| * Copyright 2011-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
| * | |
| * 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://aws.amazon.com/apache2.0 | |
| * | |
| * This file 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.amazonaws.auth; | |
| import java.io.IOException; | |
| import java.net.URISyntaxException; | |
| import java.util.Date; | |
| import com.google.gson.*; | |
| import org.apache.commons.logging.Log; | |
| import org.apache.commons.logging.LogFactory; | |
| import com.amazonaws.SdkClientException; | |
| import com.amazonaws.annotation.SdkInternalApi; | |
| import com.amazonaws.internal.CredentialsEndpointProvider; | |
| import com.amazonaws.internal.EC2CredentialsUtils; | |
| import com.amazonaws.util.DateUtils; | |
| /** | |
| * Helper class that contains the common behavior of the | |
| * CredentialsProviders that loads the credentials from a | |
| * local endpoint on an EC2 instance. | |
| */ | |
| @SdkInternalApi | |
| class EC2CredentialsFetcher { | |
| private static final Log LOG = LogFactory.getLog(EC2CredentialsFetcher.class); | |
| /** | |
| * The threshold after the last attempt to load credentials (in | |
| * milliseconds) at which credentials are attempted to be refreshed. | |
| */ | |
| private static final int REFRESH_THRESHOLD = 1000 * 60 * 60; | |
| /** | |
| * The threshold before credentials expire (in milliseconds) at which | |
| * this class will attempt to load new credentials. | |
| */ | |
| private static final int EXPIRATION_THRESHOLD = 1000 * 60 * 15; | |
| /** The name of the Json Object that contains the access key.*/ | |
| private static final String ACCESS_KEY_ID = "AccessKeyId"; | |
| /** The name of the Json Object that contains the secret access key.*/ | |
| private static final String SECRET_ACCESS_KEY = "SecretAccessKey"; | |
| /** The name of the Json Object that contains the token.*/ | |
| private static final String TOKEN = "Token"; | |
| /** The current instance profile credentials */ | |
| private volatile AWSCredentials credentials; | |
| /** The expiration for the current instance profile credentials */ | |
| private volatile Date credentialsExpiration; | |
| /** The time of the last attempt to check for new credentials */ | |
| protected volatile Date lastInstanceProfileCheck; | |
| /** Used to load the endpoint where the credentials are stored. */ | |
| private final CredentialsEndpointProvider credentialsEndpointProvider; | |
| public EC2CredentialsFetcher(CredentialsEndpointProvider credentialsEndpointProvider) { | |
| this.credentialsEndpointProvider = credentialsEndpointProvider; | |
| } | |
| public AWSCredentials getCredentials() { | |
| if (needsToLoadCredentials()) | |
| fetchCredentials(); | |
| if (expired()) { | |
| throw new SdkClientException( | |
| "The credentials received have been expired"); | |
| } | |
| return credentials; | |
| } | |
| /** | |
| * Returns true if credentials are null, credentials are within expiration or | |
| * if the last attempt to refresh credentials is beyond the refresh threshold. | |
| */ | |
| protected boolean needsToLoadCredentials() { | |
| if (credentials == null) return true; | |
| if (credentialsExpiration != null) { | |
| if (isWithinExpirationThreshold()) return true; | |
| } | |
| if (lastInstanceProfileCheck != null) { | |
| if (isPastRefreshThreshold()) return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Fetches the credentials from the endpoint. | |
| */ | |
| private synchronized void fetchCredentials() { | |
| if (!needsToLoadCredentials()) return; | |
| JsonElement accessKey; | |
| JsonElement secretKey; | |
| JsonObject node; | |
| JsonElement token; | |
| try { | |
| lastInstanceProfileCheck = new Date(); | |
| String credentialsResponse = EC2CredentialsUtils.getInstance().readResource( | |
| credentialsEndpointProvider.getCredentialsEndpoint(), | |
| credentialsEndpointProvider.getRetryPolicy(), | |
| credentialsEndpointProvider.getHeaders()); | |
| JsonParser parser = new JsonParser(); | |
| node = parser.parse(credentialsResponse).getAsJsonObject(); | |
| accessKey = node.get(ACCESS_KEY_ID); | |
| secretKey = node.get(SECRET_ACCESS_KEY); | |
| token = node.get(TOKEN); | |
| if (null == accessKey || null == secretKey) { | |
| throw new SdkClientException("Unable to load credentials."); | |
| } | |
| if (null != token) { | |
| credentials = new BasicSessionCredentials(accessKey.getAsString(), | |
| secretKey.getAsString(), token.getAsString()); | |
| } else { | |
| credentials = new BasicAWSCredentials(accessKey.getAsString(), | |
| secretKey.getAsString()); | |
| } | |
| JsonElement expirationJsonNode = node.get("Expiration"); | |
| if (null != expirationJsonNode) { | |
| /* | |
| * TODO: The expiration string comes in a different format | |
| * than what we deal with in other parts of the SDK, so we | |
| * have to convert it to the ISO8601 syntax we expect. | |
| */ | |
| String expiration = expirationJsonNode.getAsString(); | |
| expiration = expiration.replaceAll("\\+0000$", "Z"); | |
| try { | |
| credentialsExpiration = DateUtils.parseISO8601Date(expiration); | |
| } catch(Exception ex) { | |
| handleError("Unable to parse credentials expiration date from Amazon EC2 instance", ex); | |
| } | |
| } | |
| } catch (JsonSyntaxException e) { | |
| handleError("Unable to parse response returned from service endpoint", e); | |
| } catch (IOException e) { | |
| handleError("Unable to load credentials from service endpoint", e); | |
| } catch (URISyntaxException e) { | |
| handleError("Unable to load credentials from service endpoint", e); | |
| } | |
| } | |
| /** | |
| * Handles reporting or throwing an error encountered while requesting | |
| * credentials from the Amazon EC2 endpoint. The Service could be | |
| * briefly unavailable for a number of reasons, so | |
| * we need to gracefully handle falling back to valid credentials if they're | |
| * available, and only throw exceptions if we really can't recover. | |
| * | |
| * @param errorMessage | |
| * A human readable description of the error. | |
| * @param e | |
| * The error that occurred. | |
| */ | |
| private void handleError(String errorMessage, Exception e) { | |
| // If we don't have any valid credentials to fall back on, then throw an exception | |
| if (credentials == null || expired()) | |
| throw new SdkClientException(errorMessage, e); | |
| // Otherwise, just log the error and continuing using the current credentials | |
| LOG.debug(errorMessage, e); | |
| } | |
| public void refresh() { | |
| credentials = null; | |
| } | |
| /** | |
| * Returns true if the current credentials are within the expiration | |
| * threshold, and therefore, should be refreshed. | |
| */ | |
| private boolean isWithinExpirationThreshold() { | |
| return (credentialsExpiration.getTime() - System.currentTimeMillis()) < EXPIRATION_THRESHOLD; | |
| } | |
| /** | |
| * Returns true if the last attempt to refresh credentials is beyond the | |
| * refresh threshold, and therefore the credentials should attempt to be | |
| * refreshed. | |
| */ | |
| private boolean isPastRefreshThreshold() { | |
| return (System.currentTimeMillis() - lastInstanceProfileCheck.getTime()) > REFRESH_THRESHOLD; | |
| } | |
| private boolean expired() { | |
| if (credentialsExpiration != null) { | |
| if (credentialsExpiration.getTime() < System.currentTimeMillis()) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| public Date getCredentialsExpiration() { | |
| return credentialsExpiration; | |
| } | |
| @Override | |
| public String toString() { | |
| return getClass().getSimpleName(); | |
| } | |
| } |