diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6d05dadb..b2e585c45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,12 +9,12 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET Core - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v4 with: dotnet-version: '2.x' - name: install altcover diff --git a/aliyun-net-sdk-core.Tests/Units/Auth/ECSMetadataServiceCredentialsFetcher.cs b/aliyun-net-sdk-core.Tests/Units/Auth/ECSMetadataServiceCredentialsFetcher.cs index 4d0b45a2d..869ab11a7 100644 --- a/aliyun-net-sdk-core.Tests/Units/Auth/ECSMetadataServiceCredentialsFetcher.cs +++ b/aliyun-net-sdk-core.Tests/Units/Auth/ECSMetadataServiceCredentialsFetcher.cs @@ -86,10 +86,39 @@ public void Fetch2() () => { var credentials = instance.Fetch(); - ; } ); } + + [Fact] + public void FetchWithMetaDataToken() + { + var mock = new Mock + {CallBase = true}; + + var e = new ArgumentException("test"); + mock.Setup(foo => foo.GetResponse( + It.IsAny() + )).Throws(e); + + var instance = mock.Object; + var ex = Assert.Throws( + () => + { + var credentials = instance.Fetch(); + } + ); + Assert.StartsWith("Failed to get RAM session credentials from ECS metadata service. Reason: System.ArgumentException: test", ex.Message); + + var v2Fetcher = new ECSMetadataServiceCredentialsFetcher("", true, 900, 1200); + ex = Assert.Throws( + () => + { + var credentials = v2Fetcher.Fetch(); + } + ); + Assert.StartsWith("Failed to get RAM session credentials from ECS metadata service. Reason: Aliyun.Acs.Core.Exceptions.ClientException: Failed to get token from ECS Metadata Service, and fallback to IMDS v1 is disabled via the disableIMDSv1 configuration is turned on. Original error: Failed to connect ECS Metadata Service: ", ex.Message); + } [Fact] public void Fetch3() diff --git a/aliyun-net-sdk-core.Tests/Units/Auth/InstanceProfileCredentialsProvider.cs b/aliyun-net-sdk-core.Tests/Units/Auth/InstanceProfileCredentialsProvider.cs index 370eacd36..8214d91db 100644 --- a/aliyun-net-sdk-core.Tests/Units/Auth/InstanceProfileCredentialsProvider.cs +++ b/aliyun-net-sdk-core.Tests/Units/Auth/InstanceProfileCredentialsProvider.cs @@ -24,7 +24,7 @@ using Aliyun.Acs.Core.Exceptions; using Aliyun.Acs.Core.Http; using Aliyun.Acs.Core.Tests.Mock; - +using Aliyun.Acs.Core.Utils; using Moq; using Xunit; @@ -33,6 +33,21 @@ namespace Aliyun.Acs.Core.Tests.Units.Auth { public class InstanceProfileCredentialsProviderTest { + [Fact] + public void BuilderTest() + { + var cache = AuthUtils.DisableECSMetaData; + AuthUtils.DisableECSMetaData = true; + var ex = Assert.Throws(() => new InstanceProfileCredentialsProvider.Builder() + .RoleName("test") + .ReadTimeout(2000) + .ConnectTimeout(2000) + .DisableIMDSv1(false) + .Build()); + Assert.Equal("IMDS credentials is disabled.", ex.Message); + AuthUtils.DisableECSIMDSv1 = cache; + } + [Fact] public void GetCredentials1() { diff --git a/aliyun-net-sdk-core/Auth/Provider/ECSMetadataServiceCredentialsFetcher.cs b/aliyun-net-sdk-core/Auth/Provider/ECSMetadataServiceCredentialsFetcher.cs index 3de0ff3d0..10922122a 100644 --- a/aliyun-net-sdk-core/Auth/Provider/ECSMetadataServiceCredentialsFetcher.cs +++ b/aliyun-net-sdk-core/Auth/Provider/ECSMetadataServiceCredentialsFetcher.cs @@ -32,21 +32,35 @@ namespace Aliyun.Acs.Core.Auth public class ECSMetadataServiceCredentialsFetcher : AlibabaCloudCredentialsProvider { private const string URL_IN_ECS_METADATA = "/latest/meta-data/ram/security-credentials/"; - private const int DEFAULT_TIMEOUT_IN_MILLISECONDS = 5000; + private const string URL_IN_ECS_METADATA_TOKEN = "/latest/api/token"; + private const int DEFAULT_TIMEOUT_IN_MILLISECONDS = 1000; private const string ECS_METADAT_FETCH_ERROR_MSG = "Failed to get RAM session credentials from ECS metadata service."; // stands for 3600 s private const int DEFAULT_ECS_SESSION_TOKEN_DURATION_SECONDS = 3600; - private int connectionTimeoutInMilliseconds; private string credentialUrl; private string metadataServiceHost = "100.100.100.200"; private string roleName; + private readonly bool disableIMDSv1; + private const int metadataTokenDuration = 21600; + private int connectionTimeout; + private int readTimeout; public ECSMetadataServiceCredentialsFetcher() { - connectionTimeoutInMilliseconds = DEFAULT_TIMEOUT_IN_MILLISECONDS; + this.connectionTimeout = DEFAULT_TIMEOUT_IN_MILLISECONDS; + this.readTimeout = DEFAULT_TIMEOUT_IN_MILLISECONDS; + this.disableIMDSv1 = false; + } + + public ECSMetadataServiceCredentialsFetcher(string roleName, bool? disableIMDSv1, int? connectionTimeout, int? readTimeout) + { + this.roleName = roleName; + this.disableIMDSv1 = disableIMDSv1 != null && (bool)disableIMDSv1; + this.connectionTimeout = connectionTimeout == null ? 1000 : connectionTimeout.Value; + this.readTimeout = readTimeout == null ? 1000 : readTimeout.Value; } public AlibabaCloudCredentials GetCredentials() @@ -54,11 +68,12 @@ public AlibabaCloudCredentials GetCredentials() return Fetch(); } + [Obsolete] public void SetRoleName(string roleName) { if (string.IsNullOrEmpty(roleName)) { - throw new ArgumentNullException("You must specifiy a valid role name."); + throw new ArgumentNullException("You must specify a valid role name."); } this.roleName = roleName; @@ -75,22 +90,38 @@ private void SetCredentialUrl() credentialUrl = "http://" + metadataServiceHost + URL_IN_ECS_METADATA + roleName; } + [Obsolete] public void WithECSMetadataServiceHost(string host) { metadataServiceHost = host; SetCredentialUrl(); } + [Obsolete] public void WithConnectionTimeout(int milliseconds) { - connectionTimeoutInMilliseconds = milliseconds; + this.connectionTimeout = milliseconds; + this.readTimeout = milliseconds; } + [Obsolete] public string GetMetadata() { - var request = new HttpRequest(credentialUrl); + return GetMetadata(credentialUrl); + } + + private string GetMetadata(string url) + { + var request = new HttpRequest(url); request.Method = MethodType.GET; - request.SetConnectTimeoutInMilliSeconds(connectionTimeoutInMilliseconds); + request.SetConnectTimeoutInMilliSeconds(this.connectionTimeout); + request.SetReadTimeoutInMilliSeconds(this.readTimeout); + var metadataToken = this.GetMetadataToken(); + + if (metadataToken != null) + { + request.Headers.Add("X-aliyun-ecs-metadata-token", metadataToken); + } HttpResponse response; try @@ -102,6 +133,11 @@ public string GetMetadata() throw new ClientException("Failed to connect ECS Metadata Service: " + e); } + if (404 == response.Status) + { + throw new ClientException("The role name was not found in the instance."); + } + if (response.Status != 200) { throw new ClientException(ECS_METADAT_FETCH_ERROR_MSG + " HttpCode=" + response.Status); @@ -110,12 +146,60 @@ public string GetMetadata() return Encoding.UTF8.GetString(response.Content); } + private string GetMetadataToken() + { + try + { + HttpRequest httpRequest = new HttpRequest("http://" + metadataServiceHost + URL_IN_ECS_METADATA_TOKEN) + { + Method = MethodType.PUT + }; + httpRequest.SetConnectTimeoutInMilliSeconds(this.connectionTimeout); + httpRequest.SetReadTimeoutInMilliSeconds(this.readTimeout); + httpRequest.Headers.Add("X-aliyun-ecs-metadata-token-ttl-seconds", metadataTokenDuration.ToString()); + + HttpResponse httpResponse; + try + { + httpResponse = GetResponse(httpRequest); + } + catch (Exception ex) + { + throw new ClientException("Failed to connect ECS Metadata Service: " + ex); + } + if (httpResponse != null && httpResponse.Status != 200) + { + throw new ClientException("Failed to get token from ECS Metadata Service. HttpCode=" + httpResponse.Status + ", ResponseMessage=" + httpResponse.GetHttpContentString()); + } + return httpResponse.GetHttpContentString(); + } + catch (Exception ex) + { + return ThrowErrorOrReturn(ex); + } + } + + private string ThrowErrorOrReturn(Exception e) + { + if (this.disableIMDSv1) + { + throw new ClientException("Failed to get token from ECS Metadata Service, and fallback to IMDS v1 is disabled via the disableIMDSv1 configuration is turned on. Original error: " + e.Message); + } + return null; + } + public virtual InstanceProfileCredentials Fetch() { Dictionary dic; + try { - var jsonContent = GetMetadata(); + var roleName = this.roleName; + if (string.IsNullOrEmpty(this.roleName)) + { + roleName = GetMetadata("http://" + metadataServiceHost + URL_IN_ECS_METADATA); + } + var jsonContent = GetMetadata("http://" + metadataServiceHost + URL_IN_ECS_METADATA + roleName); IReader reader = new JsonReader(); dic = reader.Read(jsonContent, ""); diff --git a/aliyun-net-sdk-core/Auth/Provider/InstanceProfileCredentialsProvider.cs b/aliyun-net-sdk-core/Auth/Provider/InstanceProfileCredentialsProvider.cs index 1cc8b1bf3..99bcb61db 100644 --- a/aliyun-net-sdk-core/Auth/Provider/InstanceProfileCredentialsProvider.cs +++ b/aliyun-net-sdk-core/Auth/Provider/InstanceProfileCredentialsProvider.cs @@ -40,6 +40,23 @@ public InstanceProfileCredentialsProvider(string roleName) fetcher.SetRoleName(roleName); } + private InstanceProfileCredentialsProvider(Builder builder) + { + if (AuthUtils.DisableECSMetaData) + { + throw new ArgumentException("IMDS credentials is disabled."); + } + + this.roleName = builder.roleName ?? AuthUtils.EnvironmentEcsMetaDataDisabled; + var disableIMDSv1 = builder.disableIMDSv1 ?? AuthUtils.DisableECSIMDSv1; + this.fetcher = new ECSMetadataServiceCredentialsFetcher( + roleName, + disableIMDSv1, + builder.connectTimeout, + builder.readTimeout); + } + + public virtual AlibabaCloudCredentials GetCredentials() { try @@ -86,5 +103,42 @@ public void withFetcher(ECSMetadataServiceCredentialsFetcher fetcher) this.fetcher = fetcher; this.fetcher.SetRoleName(roleName); } + + public class Builder + { + internal string roleName; + internal bool? disableIMDSv1; + internal int? connectTimeout; + internal int? readTimeout; + + public Builder RoleName(string roleName) + { + this.roleName = roleName; + return this; + } + + public Builder DisableIMDSv1(bool? disableIMDSv1) + { + this.disableIMDSv1 = disableIMDSv1; + return this; + } + + public Builder ConnectTimeout(int? connectTimeout) + { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder ReadTimeout(int? readTimeout) + { + this.readTimeout = readTimeout; + return this; + } + + public InstanceProfileCredentialsProvider Build() + { + return new InstanceProfileCredentialsProvider(this); + } + } } } diff --git a/aliyun-net-sdk-core/Utils/AuthUtils.cs b/aliyun-net-sdk-core/Utils/AuthUtils.cs index 4de57966f..1724065df 100644 --- a/aliyun-net-sdk-core/Utils/AuthUtils.cs +++ b/aliyun-net-sdk-core/Utils/AuthUtils.cs @@ -29,10 +29,54 @@ public class AuthUtils { private static volatile string oidcToken; private static volatile string clientType = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_PROFILE"); + private static volatile string disableECSIMDSv1; + private static volatile string disableECSMetaData; + private static volatile string environmentEcsMetaDataDisabled; AuthUtils() { } + + public static string EnvironmentEcsMetaDataDisabled + { + get + { + return AuthUtils.environmentEcsMetaDataDisabled ?? Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ECS_METADATA"); + } + + set { AuthUtils.environmentEcsMetaDataDisabled = value; } + } + + public static bool DisableECSMetaData + { + get + { + if (!string.IsNullOrEmpty(AuthUtils.disableECSMetaData)) + { + return bool.Parse(AuthUtils.disableECSMetaData); + } + var env = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ECS_METADATA_DISABLED"); + return !string.IsNullOrEmpty(env) && bool.Parse(env); + } + + set { AuthUtils.disableECSMetaData = value.ToString(); } + } + + public static bool DisableECSIMDSv1 + { + get + { + if (!string.IsNullOrEmpty(AuthUtils.disableECSIMDSv1)) + { + return bool.Parse(AuthUtils.disableECSIMDSv1); + } + + var env = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_IMDSV1_DISABLED"); + return !string.IsNullOrEmpty(env) && bool.Parse(env); + } + + set { AuthUtils.disableECSIMDSv1 = value.ToString(); } + } public static string GetClientType() { @@ -40,6 +84,7 @@ public static string GetClientType() { AuthUtils.clientType = "default"; } + return AuthUtils.clientType; } @@ -51,6 +96,7 @@ public static string GetOIDCToken(string OIDCTokenFilePath) { throw new ClientException("OIDCTokenFilePath " + OIDCTokenFilePath + " does not exist."); } + try { using (var inStream = new FileStream(OIDCTokenFilePath, FileMode.Open, FileAccess.Read)) @@ -58,6 +104,7 @@ public static string GetOIDCToken(string OIDCTokenFilePath) buffer = new byte[inStream.Length]; inStream.Read(buffer, 0, buffer.Length); } + oidcToken = Encoding.UTF8.GetString(buffer); } catch (UnauthorizedAccessException) @@ -66,15 +113,15 @@ public static string GetOIDCToken(string OIDCTokenFilePath) } catch (SecurityException) { - throw new ClientException("Security Exception: Do not have the required permission. " + "OIDCTokenFilePath " + OIDCTokenFilePath); + throw new ClientException("Security Exception: Do not have the required permission. " + + "OIDCTokenFilePath " + OIDCTokenFilePath); } catch (IOException e) { Console.WriteLine(e.StackTrace); } + return oidcToken; } - - } }