AmazonECSCredentialsClient.java

/*
 * Copyright 2025 Alejandro Gonz��lez
 *
 * 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://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 net.jsign.jca;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownServiceException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import com.cedarsoftware.util.io.JsonIoException;
import com.cedarsoftware.util.io.JsonReader;

/**
 * Client to query the Elastic Container Service (ECS) credential metadata
 * endpoint for containers running in AWS.
 *
 * @since 7.2
 * @see
 * <a href="https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html">Using
 * the Amazon ECS container credentials provider</a>
 * @see
 * <a href="https://github.com/aws/aws-sdk-java-v2/blob/master/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ContainerCredentialsProvider.java">ContainerCredentialsProvider</a>
 */
class AmazonECSCredentialsClient {

    private static final URL DEFAULT_AWS_CONTAINER_SERVICE_ENDPOINT;

    private final URL endpoint;

    static {
        try {
            DEFAULT_AWS_CONTAINER_SERVICE_ENDPOINT = new URL("http://169.254.170.2");
        } catch (MalformedURLException e) {
            throw new AssertionError("Invalid default URI for AWS container credential metadata endpoint", e);
        }
    }

    /**
     * Creates a new client to query the ECS credential metadata endpoint, using
     * the endpoint URL provided by the environment variables
     * {@code AWS_CONTAINER_CREDENTIALS_RELATIVE_URI} or
     * {@code AWS_CONTAINER_CREDENTIALS_FULL_URI}.
     *
     * @throws UnknownServiceException If no valid ECS endpoint URL is
     * available.
     */
    AmazonECSCredentialsClient() throws UnknownServiceException {
        this(defaultCredentialsUrl());
    }

    /**
     * Creates a new client to query the ECS credential metadata endpoint, using
     * the specified endpoint URL.
     *
     * @param endpoint The URL of the ECS credential metadata endpoint.
     * @throws IllegalArgumentException If the endpoint URL is null or has an
     * unexpected protocol.
     */
    AmazonECSCredentialsClient(URL endpoint) {
        if (endpoint == null || (!"http".equals(endpoint.getProtocol()) && !"https".equals(endpoint.getProtocol()))) {
            throw new IllegalArgumentException(
                    "Null endpoint or unexpected protocol for AWS container credential metadata endpoint: " + endpoint
            );
        }

        this.endpoint = endpoint;
    }

    /**
     * Queries the ECS credential metadata endpoint to obtain the credentials
     * for the container.
     */
    public AmazonCredentials getCredentials() throws IOException {
        HttpURLConnection connection = (HttpURLConnection) this.endpoint.openConnection();
        connection.setConnectTimeout(3000);
        connection.setReadTimeout(3000);

        String authToken = System.getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN");
        if (authToken == null) {
            String authTokenFile = System.getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE");
            if (authTokenFile != null) {
                authToken = new String(Files.readAllBytes(Paths.get(authTokenFile)), StandardCharsets.UTF_8);
            }
        }
        if (authToken != null) {
            connection.addRequestProperty("Authorization", authToken);
        }

        int responseCode = connection.getResponseCode();
        if (responseCode != 200) {
            String responseMessage = connection.getResponseMessage();
            throw new IOException(String.format(
                    "Unexpected HTTP response code fetching AWS container credentials: %d (%s)",
                    responseCode, responseMessage == null ? "No message" : responseMessage
            ));
        }

        try {
            Map<String, String> json = JsonReader.jsonToMaps(connection.getInputStream(), new HashMap<>());
            return new AmazonCredentials(json.get("AccessKeyId"), json.get("SecretAccessKey"), json.get("Token"));
        } catch (JsonIoException e) {
            throw new IOException("Error parsing JSON response from AWS container credentials endpoint", e);
        }
    }

    /**
     * Returns the URL of the ECS credential metadata endpoint, using the
     * environment variables {@code AWS_CONTAINER_CREDENTIALS_RELATIVE_URI} or
     * {@code AWS_CONTAINER_CREDENTIALS_FULL_URI}.
     *
     * @throws UnknownServiceException If no valid ECS endpoint URL is
     * available.
     */
    private static URL defaultCredentialsUrl() throws UnknownServiceException {
        String relativeUri, fullUri;
        URL endpoint;

        if ((relativeUri = System.getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")) != null) {
            try {
                endpoint = new URL(DEFAULT_AWS_CONTAINER_SERVICE_ENDPOINT, relativeUri);
            } catch (MalformedURLException e) {
                throw new UnknownServiceException("Invalid relative URI for AWS container credential metadata endpoint: " + relativeUri);
            }
        } else if ((fullUri = System.getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")) != null) {
            try {
                endpoint = new URL(fullUri);
            } catch (MalformedURLException e) {
                throw new UnknownServiceException("Invalid full URI for AWS container credential metadata endpoint: " + fullUri);
            }
        } else {
            throw new UnknownServiceException("No AWS container credential metadata endpoint URIs available");
        }

        return endpoint;
    }
}