From 65bb25e20d8e9a891a51865faa1ab357c8041d9d Mon Sep 17 00:00:00 2001 From: Marek Chodor Date: Fri, 6 Jun 2025 09:38:21 +0000 Subject: [PATCH] fix!: support IPv6 address as custom GCE_METADATA_HOST The url for metadata server would fail if user provided IPv6 address as GCE_METADATA_HOST env. For backwards compatibility new code accepts the following: * domain, i.e. `mymetadataserver.domain.com` * domain with port, i.e. `mymetadataserver.domain.com:8080` * IPv4 address, i.e. `127.0.0.1` * IPv4 address with port, i.e. `127.0.0.1:8080` * IPv6 address, i.e. `::1` * IPv6 address within square brackers, i.e. `[::1]` * IPv6 address with port, i.e. `[::1]:8080` BREAKING CHANGE: As the new code performs URL validation, it will fallback to the `DEFAULT_METADATA_SERVER_URL` if provided env variable results in invalid URL (this is behavioral change as prior to it, the malformed URL would be passed to upper layers and result in invalid request attempt). Signed-off-by: Marek Chodor --- .../googleapis/auth/oauth2/OAuth2Utils.java | 37 +++++- .../oauth2/DefaultCredentialProviderTest.java | 110 +++++++++++++++++- 2 files changed, 143 insertions(+), 4 deletions(-) diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/auth/oauth2/OAuth2Utils.java b/google-api-client/src/main/java/com/google/api/client/googleapis/auth/oauth2/OAuth2Utils.java index f0b64a608..8bda75008 100644 --- a/google-api-client/src/main/java/com/google/api/client/googleapis/auth/oauth2/OAuth2Utils.java +++ b/google-api-client/src/main/java/com/google/api/client/googleapis/auth/oauth2/OAuth2Utils.java @@ -22,6 +22,8 @@ import com.google.api.client.util.Beta; import java.io.IOException; import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Collection; import java.util.logging.Level; @@ -103,9 +105,38 @@ public static String getMetadataServerUrl() { } static String getMetadataServerUrl(SystemEnvironmentProvider environment) { - String metadataServerAddress = environment.getEnv("GCE_METADATA_HOST"); - if (metadataServerAddress != null) { - return "http://" + metadataServerAddress; + String metadataServerHost = environment.getEnv("GCE_METADATA_HOST"); + if (metadataServerHost != null) { + try { + int idx = metadataServerHost.indexOf(":"); + if (idx >= 0 && idx == metadataServerHost.lastIndexOf(":")) { + // only one occurrence of ':' indicate this is ipv4/domain and port + return new URI( + "http", + null, + metadataServerHost.substring(0, idx), + Integer.parseInt(metadataServerHost.substring(idx + 1)), + null, + null, + null) + .toString(); + } + return new URI("http", metadataServerHost, null, null).toString(); + } catch (NumberFormatException e) { + LOGGER.log( + Level.WARNING, + "Invalid GCE_METADATA_HOST env provided, falling back to '" + + DEFAULT_METADATA_SERVER_URL + + "'.", + e); + } catch (URISyntaxException e) { + LOGGER.log( + Level.WARNING, + "Invalid GCE_METADATA_HOST env provided, falling back to '" + + DEFAULT_METADATA_SERVER_URL + + "'.", + e); + } } return DEFAULT_METADATA_SERVER_URL; } diff --git a/google-api-client/src/test/java/com/google/api/client/googleapis/auth/oauth2/DefaultCredentialProviderTest.java b/google-api-client/src/test/java/com/google/api/client/googleapis/auth/oauth2/DefaultCredentialProviderTest.java index d3ea60ccc..114aa864c 100644 --- a/google-api-client/src/test/java/com/google/api/client/googleapis/auth/oauth2/DefaultCredentialProviderTest.java +++ b/google-api-client/src/test/java/com/google/api/client/googleapis/auth/oauth2/DefaultCredentialProviderTest.java @@ -239,7 +239,33 @@ public void testDefaultCredentialNoGceCheck() throws IOException { assertEquals(0, transport.getRequestCount()); } - public void testDefaultCredentialWithCustomMetadataServerAddress() throws IOException { + public void testDefaultCredentialWithInvalidCustomMetadataServerAddress() throws IOException { + MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); + TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); + testProvider.setEnv("GCE_METADATA_HOST", "this::domain.contains.invalid.chars"); + + try { + testProvider.getDefaultCredential(transport, JSON_FACTORY); + fail("No credential expected for default test provider."); + } catch (IOException expected) { + } + assertTrue(transport.urlWasRequested("http://169.254.169.254")); + } + + public void testDefaultCredentialWithInvalidCustomMetadataServerPort() throws IOException { + MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); + TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); + testProvider.setEnv("GCE_METADATA_HOST", "test.metadata.server.address:portShouldBeANumber"); + + try { + testProvider.getDefaultCredential(transport, JSON_FACTORY); + fail("No credential expected for default test provider."); + } catch (IOException expected) { + } + assertTrue(transport.urlWasRequested("http://169.254.169.254")); + } + + public void testDefaultCredentialWithCustomMetadataServerDomainAddress() throws IOException { MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); testProvider.setEnv("GCE_METADATA_HOST", "test.metadata.server.address"); @@ -252,6 +278,88 @@ public void testDefaultCredentialWithCustomMetadataServerAddress() throws IOExce assertTrue(transport.urlWasRequested("http://test.metadata.server.address")); } + public void testDefaultCredentialWithCustomMetadataServerDomainAddressAndCustomPort() + throws IOException { + MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); + TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); + testProvider.setEnv("GCE_METADATA_HOST", "test.metadata.server.address:8080"); + + try { + testProvider.getDefaultCredential(transport, JSON_FACTORY); + fail("No credential expected for default test provider."); + } catch (IOException expected) { + } + assertTrue(transport.urlWasRequested("http://test.metadata.server.address:8080")); + } + + public void testDefaultCredentialWithCustomMetadataServerIPv4Address() throws IOException { + MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); + TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); + testProvider.setEnv("GCE_METADATA_HOST", "169.254.0.1"); + + try { + testProvider.getDefaultCredential(transport, JSON_FACTORY); + fail("No credential expected for default test provider."); + } catch (IOException expected) { + } + assertTrue(transport.urlWasRequested("http://169.254.0.1")); + } + + public void testDefaultCredentialWithCustomMetadataServerIPv4AddressAndCustomPort() + throws IOException { + MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); + TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); + testProvider.setEnv("GCE_METADATA_HOST", "169.254.0.1:8080"); + + try { + testProvider.getDefaultCredential(transport, JSON_FACTORY); + fail("No credential expected for default test provider."); + } catch (IOException expected) { + } + assertTrue(transport.urlWasRequested("http://169.254.0.1:8080")); + } + + public void testDefaultCredentialWithCustomMetadataServerIPv6Address() throws IOException { + MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); + TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); + testProvider.setEnv("GCE_METADATA_HOST", "fe80::1"); + + try { + testProvider.getDefaultCredential(transport, JSON_FACTORY); + fail("No credential expected for default test provider."); + } catch (IOException expected) { + } + assertTrue(transport.urlWasRequested("http://[fe80::1]")); + } + + public void testDefaultCredentialWithCustomMetadataServerIPv6AddressProvidedWithBrackets() + throws IOException { + MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); + TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); + testProvider.setEnv("GCE_METADATA_HOST", "[fe80::1]"); + + try { + testProvider.getDefaultCredential(transport, JSON_FACTORY); + fail("No credential expected for default test provider."); + } catch (IOException expected) { + } + assertTrue(transport.urlWasRequested("http://[fe80::1]")); + } + + public void testDefaultCredentialWithCustomMetadataServerIPv6AddressAndCustomPort() + throws IOException { + MockRequestUrlRecordingTransport transport = new MockRequestUrlRecordingTransport(); + TestDefaultCredentialProvider testProvider = new TestDefaultCredentialProvider(); + testProvider.setEnv("GCE_METADATA_HOST", "[fe80::1]:8080"); + + try { + testProvider.getDefaultCredential(transport, JSON_FACTORY); + fail("No credential expected for default test provider."); + } catch (IOException expected) { + } + assertTrue(transport.urlWasRequested("http://[fe80::1]:8080")); + } + public void testDefaultCredentialNonExistentFileThrows() throws Exception { File nonExistentFile = new java.io.File(getTempDirectory(), "DefaultCredentialBadFile.json"); assertFalse(nonExistentFile.exists());