Skip to content

Commit 17a2f12

Browse files
committed
feat: support IMDSv2 for ECS metadata
1 parent f01a538 commit 17a2f12

File tree

6 files changed

+247
-18
lines changed

6 files changed

+247
-18
lines changed

.github/workflows/ci.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ jobs:
1212
runs-on: ubuntu-latest
1313

1414
steps:
15-
- uses: actions/checkout@v3
15+
- uses: actions/checkout@v4
1616
- name: Setup .NET Core
17-
uses: actions/setup-dotnet@v2
17+
uses: actions/setup-dotnet@v4
1818
with:
1919
dotnet-version: '2.x'
2020
- name: install altcover
21-
run: dotnet tool install --global altcover.visualizer --version 8.6.14
21+
run: sudo apt-get update && sudo apt-get install -y libicu-dev && dotnet tool install --global altcover.visualizer --version 8.6.14
2222
- name: Install dependencies
2323
run: cd aliyun-net-sdk-core.Tests/ && dotnet add package AltCover --version 8.6.14 && cd ../ && dotnet restore && dotnet build
2424
- name: Test

aliyun-net-sdk-core.Tests/Units/Auth/ECSMetadataServiceCredentialsFetcher.cs

+31-2
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,39 @@ public void Fetch2()
8686
() =>
8787
{
8888
var credentials = instance.Fetch();
89-
;
9089
}
9190
);
9291
}
92+
93+
[Fact]
94+
public void FetchWithMetaDataToken()
95+
{
96+
var mock = new Mock<ECSMetadataServiceCredentialsFetcher>
97+
{CallBase = true};
98+
99+
var e = new ArgumentException("test");
100+
mock.Setup(foo => foo.GetResponse(
101+
It.IsAny<HttpRequest>()
102+
)).Throws(e);
103+
104+
var instance = mock.Object;
105+
var ex = Assert.Throws<ClientException>(
106+
() =>
107+
{
108+
var credentials = instance.Fetch();
109+
}
110+
);
111+
Assert.StartsWith("Failed to connect ECS Metadata Service: System.ArgumentException: test", ex.Message);
112+
113+
var v2Fetcher = new ECSMetadataServiceCredentialsFetcher("", true, 900, 1200);
114+
ex = Assert.Throws<ClientException>(
115+
() =>
116+
{
117+
var credentials = v2Fetcher.Fetch();
118+
}
119+
);
120+
Assert.StartsWith("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);
121+
}
93122

94123
[Fact]
95124
public void Fetch3()
@@ -279,7 +308,7 @@ public void Set()
279308
{
280309
var instance = new ECSMetadataServiceCredentialsFetcher();
281310

282-
Assert.Throws<ArgumentNullException>(
311+
Assert.Throws<ArgumentException>(
283312
() => { instance.SetRoleName(""); }
284313
);
285314
instance.WithECSMetadataServiceHost("host");

aliyun-net-sdk-core.Tests/Units/Auth/InstanceProfileCredentialsProvider.cs

+16-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
using Aliyun.Acs.Core.Exceptions;
2525
using Aliyun.Acs.Core.Http;
2626
using Aliyun.Acs.Core.Tests.Mock;
27-
27+
using Aliyun.Acs.Core.Utils;
2828
using Moq;
2929

3030
using Xunit;
@@ -33,6 +33,21 @@ namespace Aliyun.Acs.Core.Tests.Units.Auth
3333
{
3434
public class InstanceProfileCredentialsProviderTest
3535
{
36+
[Fact]
37+
public void BuilderTest()
38+
{
39+
var cache = AuthUtils.DisableECSMetaData;
40+
AuthUtils.DisableECSMetaData = true;
41+
var ex = Assert.Throws<ArgumentException>(() => new InstanceProfileCredentialsProvider.Builder()
42+
.RoleName("test")
43+
.ReadTimeout(2000)
44+
.ConnectTimeout(2000)
45+
.DisableIMDSv1(false)
46+
.Build());
47+
Assert.Equal("IMDS credentials is disabled.", ex.Message);
48+
AuthUtils.DisableECSIMDSv1 = cache;
49+
}
50+
3651
[Fact]
3752
public void GetCredentials1()
3853
{

aliyun-net-sdk-core/Auth/Provider/ECSMetadataServiceCredentialsFetcher.cs

+93-9
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,48 @@ namespace Aliyun.Acs.Core.Auth
3232
public class ECSMetadataServiceCredentialsFetcher : AlibabaCloudCredentialsProvider
3333
{
3434
private const string URL_IN_ECS_METADATA = "/latest/meta-data/ram/security-credentials/";
35-
private const int DEFAULT_TIMEOUT_IN_MILLISECONDS = 5000;
35+
private const string URL_IN_ECS_METADATA_TOKEN = "/latest/api/token";
36+
private const int DEFAULT_TIMEOUT_IN_MILLISECONDS = 1000;
3637

3738
private const string ECS_METADAT_FETCH_ERROR_MSG =
3839
"Failed to get RAM session credentials from ECS metadata service.";
3940

4041
// stands for 3600 s
4142
private const int DEFAULT_ECS_SESSION_TOKEN_DURATION_SECONDS = 3600;
42-
private int connectionTimeoutInMilliseconds;
4343
private string credentialUrl;
4444
private string metadataServiceHost = "100.100.100.200";
4545
private string roleName;
46+
private readonly bool disableIMDSv1;
47+
private const int metadataTokenDuration = 21600;
48+
private int connectionTimeout;
49+
private int readTimeout;
4650

4751
public ECSMetadataServiceCredentialsFetcher()
4852
{
49-
connectionTimeoutInMilliseconds = DEFAULT_TIMEOUT_IN_MILLISECONDS;
53+
this.connectionTimeout = DEFAULT_TIMEOUT_IN_MILLISECONDS;
54+
this.readTimeout = DEFAULT_TIMEOUT_IN_MILLISECONDS;
55+
this.disableIMDSv1 = false;
56+
}
57+
58+
public ECSMetadataServiceCredentialsFetcher(string roleName, bool? disableIMDSv1, int? connectionTimeout, int? readTimeout)
59+
{
60+
this.roleName = roleName;
61+
this.disableIMDSv1 = disableIMDSv1 != null && (bool)disableIMDSv1;
62+
this.connectionTimeout = connectionTimeout == null ? 1000 : connectionTimeout.Value;
63+
this.readTimeout = readTimeout == null ? 1000 : readTimeout.Value;
5064
}
5165

5266
public AlibabaCloudCredentials GetCredentials()
5367
{
5468
return Fetch();
5569
}
5670

71+
[Obsolete]
5772
public void SetRoleName(string roleName)
5873
{
5974
if (string.IsNullOrEmpty(roleName))
6075
{
61-
throw new ArgumentNullException("You must specifiy a valid role name.");
76+
throw new ArgumentException("You must specify a valid role name.");
6277
}
6378

6479
this.roleName = roleName;
@@ -75,33 +90,54 @@ private void SetCredentialUrl()
7590
credentialUrl = "http://" + metadataServiceHost + URL_IN_ECS_METADATA + roleName;
7691
}
7792

93+
[Obsolete]
7894
public void WithECSMetadataServiceHost(string host)
7995
{
8096
metadataServiceHost = host;
8197
SetCredentialUrl();
8298
}
8399

100+
[Obsolete]
84101
public void WithConnectionTimeout(int milliseconds)
85102
{
86-
connectionTimeoutInMilliseconds = milliseconds;
103+
this.connectionTimeout = milliseconds;
104+
this.readTimeout = milliseconds;
87105
}
88106

107+
[Obsolete]
89108
public string GetMetadata()
90109
{
91-
var request = new HttpRequest(credentialUrl);
110+
return GetMetadata(credentialUrl);
111+
}
112+
113+
private string GetMetadata(string url)
114+
{
115+
var request = new HttpRequest(url);
92116
request.Method = MethodType.GET;
93-
request.SetConnectTimeoutInMilliSeconds(connectionTimeoutInMilliseconds);
117+
request.SetConnectTimeoutInMilliSeconds(this.connectionTimeout);
118+
request.SetReadTimeoutInMilliSeconds(this.readTimeout);
119+
var metadataToken = this.GetMetadataToken();
120+
121+
if (metadataToken != null)
122+
{
123+
request.Headers.Add("X-aliyun-ecs-metadata-token", metadataToken);
124+
}
94125

95126
HttpResponse response;
96127
try
97128
{
98129
response = GetResponse(request);
99130
}
100-
catch (WebException e)
131+
catch (Exception e)
101132
{
102133
throw new ClientException("Failed to connect ECS Metadata Service: " + e);
103134
}
104135

136+
if (404 == response.Status)
137+
{
138+
throw new ClientException("The role name was not found in the instance.");
139+
}
140+
105141
if (response.Status != 200)
106142
{
107143
throw new ClientException(ECS_METADAT_FETCH_ERROR_MSG + " HttpCode=" + response.Status);
@@ -110,12 +146,60 @@ public string GetMetadata()
110146
return Encoding.UTF8.GetString(response.Content);
111147
}
112148

149+
private string GetMetadataToken()
150+
{
151+
try
152+
{
153+
HttpRequest httpRequest = new HttpRequest("http://" + metadataServiceHost + URL_IN_ECS_METADATA_TOKEN)
154+
{
155+
Method = MethodType.PUT
156+
};
157+
httpRequest.SetConnectTimeoutInMilliSeconds(this.connectionTimeout);
158+
httpRequest.SetReadTimeoutInMilliSeconds(this.readTimeout);
159+
httpRequest.Headers.Add("X-aliyun-ecs-metadata-token-ttl-seconds", metadataTokenDuration.ToString());
160+
161+
HttpResponse httpResponse;
162+
try
163+
{
164+
httpResponse = GetResponse(httpRequest);
165+
}
166+
catch (Exception ex)
167+
{
168+
throw new ClientException("Failed to connect ECS Metadata Service: " + ex);
169+
}
170+
if (httpResponse != null && httpResponse.Status != 200)
171+
{
172+
throw new ClientException("Failed to get token from ECS Metadata Service. HttpCode=" + httpResponse.Status + ", ResponseMessage=" + httpResponse.GetHttpContentString());
173+
}
174+
return httpResponse.GetHttpContentString();
175+
}
176+
catch (Exception ex)
177+
{
178+
return ThrowErrorOrReturn(ex);
179+
}
180+
}
181+
182+
private string ThrowErrorOrReturn(Exception e)
183+
{
184+
if (this.disableIMDSv1)
185+
{
186+
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);
187+
}
188+
return null;
189+
}
190+
113191
public virtual InstanceProfileCredentials Fetch()
114192
{
115193
Dictionary<string, string> dic;
194+
var roleName = this.roleName;
195+
if (string.IsNullOrEmpty(this.roleName))
196+
{
197+
roleName = GetMetadata("http://" + metadataServiceHost + URL_IN_ECS_METADATA);
198+
}
199+
116200
try
117201
{
118-
var jsonContent = GetMetadata();
202+
var jsonContent = GetMetadata("http://" + metadataServiceHost + URL_IN_ECS_METADATA + roleName);
119203

120204
IReader reader = new JsonReader();
121205
dic = reader.Read(jsonContent, "");

aliyun-net-sdk-core/Auth/Provider/InstanceProfileCredentialsProvider.cs

+54
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ public InstanceProfileCredentialsProvider(string roleName)
4040
fetcher.SetRoleName(roleName);
4141
}
4242

43+
private InstanceProfileCredentialsProvider(Builder builder)
44+
{
45+
if (AuthUtils.DisableECSMetaData)
46+
{
47+
throw new ArgumentException("IMDS credentials is disabled.");
48+
}
49+
50+
this.roleName = builder.roleName ?? AuthUtils.EnvironmentEcsMetaDataDisabled;
51+
var disableIMDSv1 = builder.disableIMDSv1 ?? AuthUtils.DisableECSIMDSv1;
52+
this.fetcher = new ECSMetadataServiceCredentialsFetcher(
53+
roleName,
54+
disableIMDSv1,
55+
builder.connectTimeout,
56+
builder.readTimeout);
57+
}
58+
59+
4360
public virtual AlibabaCloudCredentials GetCredentials()
4461
{
4562
try
@@ -86,5 +103,42 @@ public void withFetcher(ECSMetadataServiceCredentialsFetcher fetcher)
86103
this.fetcher = fetcher;
87104
this.fetcher.SetRoleName(roleName);
88105
}
106+
107+
public class Builder
108+
{
109+
internal string roleName;
110+
internal bool? disableIMDSv1;
111+
internal int? connectTimeout;
112+
internal int? readTimeout;
113+
114+
public Builder RoleName(string roleName)
115+
{
116+
this.roleName = roleName;
117+
return this;
118+
}
119+
120+
public Builder DisableIMDSv1(bool? disableIMDSv1)
121+
{
122+
this.disableIMDSv1 = disableIMDSv1;
123+
return this;
124+
}
125+
126+
public Builder ConnectTimeout(int? connectTimeout)
127+
{
128+
this.connectTimeout = connectTimeout;
129+
return this;
130+
}
131+
132+
public Builder ReadTimeout(int? readTimeout)
133+
{
134+
this.readTimeout = readTimeout;
135+
return this;
136+
}
137+
138+
public InstanceProfileCredentialsProvider Build()
139+
{
140+
return new InstanceProfileCredentialsProvider(this);
141+
}
142+
}
89143
}
90144
}

0 commit comments

Comments
 (0)