diff --git a/s3/src/main/java/ch/cyberduck/core/auth/AWSSessionCredentialsRetriever.java b/s3/src/main/java/ch/cyberduck/core/auth/AWSSessionCredentialsRetriever.java index 8112fc11b9d..4b7b973f52d 100644 --- a/s3/src/main/java/ch/cyberduck/core/auth/AWSSessionCredentialsRetriever.java +++ b/s3/src/main/java/ch/cyberduck/core/auth/AWSSessionCredentialsRetriever.java @@ -68,14 +68,18 @@ public AWSSessionCredentialsRetriever(final X509TrustManager trust, final X509Ke @Override public Credentials get() throws BackgroundException { - log.debug("Configure credentials from {}", url); + log.debug("Fetching AWS session credentials from URL: {}", url); + // Parse URL to get hostname for SSL configuration, but use original URL for the request + // to preserve encoded characters and path structure (e.g., %2F and double slashes) final Host address = new HostParser(ProtocolFactory.get()).get(url); final HttpConnectionPoolBuilder builder = new HttpConnectionPoolBuilder(address, new ThreadLocalHostnameDelegatingTrustManager(trust, address.getHostname()), key, ProxyFactory.get()); final HttpClientBuilder configuration = builder.build(ProxyFactory.get(), new DisabledTranscriptListener(), new DisabledLoginCallback()); try (CloseableHttpClient client = configuration.build()) { - final HttpRequestBase resource = new HttpGet(new HostUrlProvider().withUsername(false).withPath(true).get(address)); + // Use the original URL directly to preserve encoding and structure + log.info("Making HTTP GET request to: {}", url); + final HttpRequestBase resource = new HttpGet(url); return client.execute(resource, response -> { switch(response.getStatusLine().getStatusCode()) { case HttpStatus.SC_OK: diff --git a/s3/src/main/java/ch/cyberduck/core/s3/S3CredentialsConfigurator.java b/s3/src/main/java/ch/cyberduck/core/s3/S3CredentialsConfigurator.java index f3588047dea..adf7dabe08b 100644 --- a/s3/src/main/java/ch/cyberduck/core/s3/S3CredentialsConfigurator.java +++ b/s3/src/main/java/ch/cyberduck/core/s3/S3CredentialsConfigurator.java @@ -78,6 +78,19 @@ public S3CredentialsConfigurator(final Local directory) { @Override public Credentials configure(final Host host) { final Credentials credentials = new Credentials(host.getCredentials()); + // Check for HTTP credentials URL in bookmark + final String httpCredentialsUrl = host.getProperty("aws.credentials.http.url"); + log.debug("Retrieved aws.credentials.http.url property value: '{}'", httpCredentialsUrl); + if(StringUtils.isNotBlank(httpCredentialsUrl)) { + log.debug("HTTP credentials URL configured: {}. Returning placeholder credentials to skip credential validation.", httpCredentialsUrl); + // Return credentials with placeholder tokens AND username/password to pass validation + // The actual credentials will be fetched via HTTP during session connect + return credentials + .setUsername("http-credentials-placeholder") + .setPassword("http-credentials-placeholder") + .setTokens(new TemporaryAccessTokens("http-credentials-placeholder", "http-credentials-placeholder", "http-credentials-placeholder", -1L)); + } + log.info("No HTTP credentials URL found, continuing with AWS CLI profile lookup"); final BasicProfile profile = profiles.entrySet().stream().filter(entry -> { // Matching access key or profile name if(StringUtils.equals(entry.getKey(), credentials.getUsername())) { diff --git a/s3/src/main/java/ch/cyberduck/core/s3/S3Session.java b/s3/src/main/java/ch/cyberduck/core/s3/S3Session.java index 85abefceb6d..961fc3cfb46 100644 --- a/s3/src/main/java/ch/cyberduck/core/s3/S3Session.java +++ b/s3/src/main/java/ch/cyberduck/core/s3/S3Session.java @@ -248,6 +248,18 @@ public void process(final HttpRequest request, final HttpContext context) { protected S3CredentialsStrategy configureCredentialsStrategy(final HttpClientBuilder configuration, final LoginCallback prompt) throws LoginCanceledException { + // Check for custom HTTP credentials URL in bookmark (stored in Custom map) + log.info("Checking for aws.credentials.http.url property in bookmark"); + log.info("Host custom properties: {}", host.getCustom()); + final String httpCredentialsUrl = host.getProperty("aws.credentials.http.url"); + log.info("Retrieved aws.credentials.http.url value: {}", httpCredentialsUrl); + if(StringUtils.isNotBlank(httpCredentialsUrl)) { + log.info("Configure credentials from bookmark HTTP endpoint {}", httpCredentialsUrl); + final AWSSessionCredentialsRetriever retriever = new AWSSessionCredentialsRetriever(trust, key, httpCredentialsUrl); + log.info("Created AWSSessionCredentialsRetriever: {}", retriever); + return retriever; + } + log.info("No HTTP credentials URL found, continuing with other authentication methods"); if(host.getProtocol().isOAuthConfigurable()) { final OAuth2RequestInterceptor oauth = new OAuth2RequestInterceptor(configuration.build(), host, prompt) .withRedirectUri(host.getProtocol().getOAuthRedirectUrl()); diff --git a/s3/src/test/java/ch/cyberduck/core/s3/S3HttpCredentialsTest.java b/s3/src/test/java/ch/cyberduck/core/s3/S3HttpCredentialsTest.java new file mode 100644 index 00000000000..9d8826d78a8 --- /dev/null +++ b/s3/src/test/java/ch/cyberduck/core/s3/S3HttpCredentialsTest.java @@ -0,0 +1,56 @@ +package ch.cyberduck.core.s3; + +import ch.cyberduck.core.DisabledCancelCallback; +import ch.cyberduck.core.DisabledHostKeyCallback; +import ch.cyberduck.core.DisabledLoginCallback; +import ch.cyberduck.core.Host; +import ch.cyberduck.core.Profile; +import ch.cyberduck.core.ProtocolFactory; +import ch.cyberduck.core.serializer.impl.dd.ProfilePlistReader; +import ch.cyberduck.core.ssl.DefaultX509KeyManager; +import ch.cyberduck.core.ssl.DisabledX509TrustManager; + +import org.junit.Test; + +import java.util.Collections; +import java.util.HashSet; + +import static org.junit.Assert.*; + +public class S3HttpCredentialsTest { + + @Test + public void testHttpCredentialsUrlProperty() throws Exception { + final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol()))); + final Profile profile = new ProfilePlistReader(factory).read( + this.getClass().getResourceAsStream("/S3 (Credentials from Instance Metadata).cyberduckprofile")); + final Host host = new Host(profile, profile.getDefaultHostname()); + + // Test that the profile's Context field is readable + assertNotNull(host.getProtocol().getContext()); + assertEquals("http://169.254.169.254/latest/meta-data/iam/security-credentials/s3access", + host.getProtocol().getContext()); + + // Test that custom property can be set and retrieved + host.setProperty("aws.credentials.http.url", "https://example.com/credentials"); + assertEquals("https://example.com/credentials", host.getProperty("aws.credentials.http.url")); + } + + @Test + public void testBookmarkWithHttpCredentialsUrl() throws Exception { + final S3Protocol protocol = new S3Protocol(); + final Host host = new Host(protocol, "content-repo-prod-contentsupplier-touch.s3.amazonaws.com"); + + // Simulate bookmark with HTTP credentials URL in Custom dict + host.setProperty("aws.credentials.http.url", + "https://up.example.com/VMHGTtd0BDYpcZgWU2arm6SwElXsiOCK"); + + // Verify the property is accessible + assertEquals("https://up.example.com/VMHGTtd0BDYpcZgWU2arm6SwElXsiOCK", + host.getProperty("aws.credentials.http.url")); + + // Verify session can be created (without actually connecting) + final S3Session session = new S3Session(host, new DisabledX509TrustManager(), new DefaultX509KeyManager()); + assertNotNull(session); + } +}