1 | /* |
2 | * Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"). |
5 | * You may not use this file except in compliance with the License. |
6 | * A copy of the License is located at |
7 | * |
8 | * http://aws.amazon.com/apache2.0 |
9 | * |
10 | * or in the "license" file accompanying this file. This file is distributed |
11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either |
12 | * express or implied. See the License for the specific language governing |
13 | * permissions and limitations under the License. |
14 | */ |
15 | |
16 | #include <aws/core/internal/AWSHttpResourceClient.h> |
17 | #include <aws/core/client/DefaultRetryStrategy.h> |
18 | #include <aws/core/http/HttpClient.h> |
19 | #include <aws/core/http/HttpClientFactory.h> |
20 | #include <aws/core/http/HttpResponse.h> |
21 | #include <aws/core/utils/logging/LogMacros.h> |
22 | #include <aws/core/utils/StringUtils.h> |
23 | #include <aws/core/utils/HashingUtils.h> |
24 | #include <aws/core/platform/Environment.h> |
25 | #include <aws/core/client/AWSError.h> |
26 | #include <aws/core/client/CoreErrors.h> |
27 | #include <aws/core/utils/xml/XmlSerializer.h> |
28 | #include <mutex> |
29 | #include <sstream> |
30 | |
31 | using namespace Aws; |
32 | using namespace Aws::Utils; |
33 | using namespace Aws::Utils::Logging; |
34 | using namespace Aws::Utils::Xml; |
35 | using namespace Aws::Http; |
36 | using namespace Aws::Client; |
37 | using namespace Aws::Internal; |
38 | |
39 | static const char EC2_SECURITY_CREDENTIALS_RESOURCE[] = "/latest/meta-data/iam/security-credentials" ; |
40 | static const char EC2_REGION_RESOURCE[] = "/latest/meta-data/placement/availability-zone" ; |
41 | static const char EC2_IMDS_TOKEN_RESOURCE[] = "/latest/api/token" ; |
42 | static const char EC2_IMDS_TOKEN_TTL_DEFAULT_VALUE[] = "21600" ; |
43 | static const char [] = "x-aws-ec2-metadata-token-ttl-seconds" ; |
44 | static const char [] = "x-aws-ec2-metadata-token" ; |
45 | static const char RESOURCE_CLIENT_CONFIGURATION_ALLOCATION_TAG[] = "AWSHttpResourceClient" ; |
46 | static const char EC2_METADATA_CLIENT_LOG_TAG[] = "EC2MetadataClient" ; |
47 | static const char ECS_CREDENTIALS_CLIENT_LOG_TAG[] = "ECSCredentialsClient" ; |
48 | |
49 | namespace Aws |
50 | { |
51 | namespace Client |
52 | { |
53 | Aws::String ComputeUserAgentString(); |
54 | } |
55 | } |
56 | |
57 | static ClientConfiguration MakeDefaultHttpResourceClientConfiguration(const char *logtag) |
58 | { |
59 | ClientConfiguration res; |
60 | |
61 | res.maxConnections = 2; |
62 | res.scheme = Scheme::HTTP; |
63 | |
64 | #if defined(WIN32) && defined(BYPASS_DEFAULT_PROXY) |
65 | // For security reasons, we must bypass any proxy settings when fetching sensitive information, for example |
66 | // user credentials. On Windows, IXMLHttpRequest2 does not support bypassing proxy settings, therefore, |
67 | // we force using WinHTTP client. On POSIX systems, CURL is set to bypass proxy settings by default. |
68 | res.httpLibOverride = TransferLibType::WIN_HTTP_CLIENT; |
69 | AWS_LOGSTREAM_INFO(logtag, "Overriding the current HTTP client to WinHTTP to bypass proxy settings." ); |
70 | #else |
71 | (void) logtag; // To disable warning about unused variable |
72 | #endif |
73 | // Explicitly set the proxy settings to empty/zero to avoid relying on defaults that could potentially change |
74 | // in the future. |
75 | res.proxyHost = "" ; |
76 | res.proxyUserName = "" ; |
77 | res.proxyPassword = "" ; |
78 | res.proxyPort = 0; |
79 | |
80 | // EC2MetadataService throttles by delaying the response so the service client should set a large read timeout. |
81 | // EC2MetadataService delay is in order of seconds so it only make sense to retry after a couple of seconds. |
82 | res.connectTimeoutMs = 1000; |
83 | res.requestTimeoutMs = 5000; |
84 | res.retryStrategy = Aws::MakeShared<DefaultRetryStrategy>(RESOURCE_CLIENT_CONFIGURATION_ALLOCATION_TAG, 4, 1000); |
85 | |
86 | return res; |
87 | } |
88 | |
89 | AWSHttpResourceClient::AWSHttpResourceClient(const Aws::Client::ClientConfiguration& clientConfiguration, const char* logtag) |
90 | : m_logtag(logtag), m_retryStrategy(clientConfiguration.retryStrategy), m_httpClient(nullptr) |
91 | { |
92 | AWS_LOGSTREAM_INFO(m_logtag.c_str(), |
93 | "Creating AWSHttpResourceClient with max connections " |
94 | << clientConfiguration.maxConnections |
95 | << " and scheme " |
96 | << SchemeMapper::ToString(clientConfiguration.scheme)); |
97 | |
98 | m_httpClient = CreateHttpClient(clientConfiguration); |
99 | } |
100 | |
101 | AWSHttpResourceClient::AWSHttpResourceClient(const char* logtag) |
102 | : AWSHttpResourceClient(MakeDefaultHttpResourceClientConfiguration(logtag), logtag) |
103 | { |
104 | } |
105 | |
106 | AWSHttpResourceClient::~AWSHttpResourceClient() |
107 | { |
108 | } |
109 | |
110 | Aws::String AWSHttpResourceClient::GetResource(const char* endpoint, const char* resource, const char* authToken) const |
111 | { |
112 | return GetResourceWithAWSWebServiceResult(endpoint, resource, authToken).GetPayload(); |
113 | } |
114 | |
115 | AmazonWebServiceResult<Aws::String> AWSHttpResourceClient::GetResourceWithAWSWebServiceResult(const char *endpoint, const char *resource, const char *authToken) const |
116 | { |
117 | Aws::StringStream ss; |
118 | ss << endpoint << resource; |
119 | std::shared_ptr<HttpRequest> request(CreateHttpRequest(ss.str(), HttpMethod::HTTP_GET, |
120 | Aws::Utils::Stream::DefaultResponseStreamFactoryMethod)); |
121 | |
122 | request->SetUserAgent(ComputeUserAgentString()); |
123 | |
124 | if (authToken) |
125 | { |
126 | request->SetHeaderValue(Aws::Http::AWS_AUTHORIZATION_HEADER, authToken); |
127 | } |
128 | |
129 | return GetResourceWithAWSWebServiceResult(request); |
130 | } |
131 | |
132 | AmazonWebServiceResult<Aws::String> AWSHttpResourceClient::GetResourceWithAWSWebServiceResult(const std::shared_ptr<HttpRequest> &httpRequest) const |
133 | { |
134 | AWS_LOGSTREAM_TRACE(m_logtag.c_str(), "Retrieving credentials from " << httpRequest->GetURIString()); |
135 | |
136 | for (long retries = 0;; retries++) |
137 | { |
138 | std::shared_ptr<HttpResponse> response(m_httpClient->MakeRequest(httpRequest)); |
139 | |
140 | if (response && response->GetResponseCode() == HttpResponseCode::OK) |
141 | { |
142 | Aws::IStreamBufIterator eos; |
143 | return {Aws::String(Aws::IStreamBufIterator(response->GetResponseBody()), eos), response ? response->GetHeaders() : HeaderValueCollection(), HttpResponseCode::OK}; |
144 | } |
145 | |
146 | const Aws::Client::AWSError<Aws::Client::CoreErrors> error = [this, &response]() { |
147 | if (!response || response->GetResponseBody().tellp() < 1) |
148 | { |
149 | AWS_LOGSTREAM_ERROR(m_logtag.c_str(), "Http request to retrieve credentials failed" ); |
150 | return AWSError<CoreErrors>(CoreErrors::NETWORK_CONNECTION, true); // Retryable |
151 | } |
152 | else if (m_errorMarshaller) |
153 | { |
154 | return m_errorMarshaller->Marshall(*response); |
155 | } |
156 | else |
157 | { |
158 | const auto responseCode = response->GetResponseCode(); |
159 | |
160 | AWS_LOGSTREAM_ERROR(m_logtag.c_str(), "Http request to retrieve credentials failed with error code " |
161 | << static_cast<int>(responseCode)); |
162 | return CoreErrorsMapper::GetErrorForHttpResponseCode(responseCode); |
163 | } |
164 | }(); |
165 | |
166 | if (!m_retryStrategy->ShouldRetry(error, retries)) |
167 | { |
168 | AWS_LOGSTREAM_ERROR(m_logtag.c_str(), "Can not retrive resource from " << httpRequest->GetURIString()); |
169 | return {{}, response ? response->GetHeaders() : HeaderValueCollection(), error.GetResponseCode()}; |
170 | } |
171 | auto sleepMillis = m_retryStrategy->CalculateDelayBeforeNextRetry(error, retries); |
172 | AWS_LOGSTREAM_WARN(m_logtag.c_str(), "Request failed, now waiting " << sleepMillis << " ms before attempting again." ); |
173 | m_httpClient->RetryRequestSleep(std::chrono::milliseconds(sleepMillis)); |
174 | } |
175 | } |
176 | |
177 | EC2MetadataClient::EC2MetadataClient(const char* endpoint) |
178 | : AWSHttpResourceClient(EC2_METADATA_CLIENT_LOG_TAG), m_endpoint(endpoint), m_tokenRequired(true) |
179 | { |
180 | } |
181 | |
182 | EC2MetadataClient::EC2MetadataClient(const Aws::Client::ClientConfiguration &clientConfiguration, const char *endpoint) |
183 | : AWSHttpResourceClient(clientConfiguration, EC2_METADATA_CLIENT_LOG_TAG), m_endpoint(endpoint), m_tokenRequired(true) |
184 | { |
185 | } |
186 | |
187 | EC2MetadataClient::~EC2MetadataClient() |
188 | { |
189 | |
190 | } |
191 | |
192 | Aws::String EC2MetadataClient::GetResource(const char* resourcePath) const |
193 | { |
194 | return GetResource(m_endpoint.c_str(), resourcePath, nullptr/*authToken*/); |
195 | } |
196 | |
197 | Aws::String EC2MetadataClient::GetDefaultCredentials() const |
198 | { |
199 | std::unique_lock<std::recursive_mutex> locker(m_tokenMutex); |
200 | if (m_tokenRequired) |
201 | { |
202 | return GetDefaultCredentialsSecurely(); |
203 | } |
204 | |
205 | AWS_LOGSTREAM_TRACE(m_logtag.c_str(), "Getting default credentials for ec2 instance" ); |
206 | auto result = GetResourceWithAWSWebServiceResult(m_endpoint.c_str(), EC2_SECURITY_CREDENTIALS_RESOURCE, nullptr); |
207 | Aws::String credentialsString = result.GetPayload(); |
208 | auto httpResponseCode = result.GetResponseCode(); |
209 | |
210 | // Note, if service is insane, it might return 404 for our initial secure call, |
211 | // then when we fall back to insecure call, it might return 401 ask for secure call, |
212 | // Then, SDK might get into a recursive loop call situation between secure and insecure call. |
213 | if (httpResponseCode == Http::HttpResponseCode::UNAUTHORIZED) |
214 | { |
215 | m_tokenRequired = true; |
216 | return {}; |
217 | } |
218 | locker.unlock(); |
219 | |
220 | Aws::String trimmedCredentialsString = StringUtils::Trim(credentialsString.c_str()); |
221 | if (trimmedCredentialsString.empty()) return {}; |
222 | |
223 | Aws::Vector<Aws::String> securityCredentials = StringUtils::Split(trimmedCredentialsString, '\n'); |
224 | |
225 | AWS_LOGSTREAM_DEBUG(m_logtag.c_str(), "Calling EC2MetadataService resource, " << EC2_SECURITY_CREDENTIALS_RESOURCE |
226 | << " returned credential string " << trimmedCredentialsString); |
227 | |
228 | if (securityCredentials.size() == 0) |
229 | { |
230 | AWS_LOGSTREAM_WARN(m_logtag.c_str(), "Initial call to ec2Metadataservice to get credentials failed" ); |
231 | return {}; |
232 | } |
233 | |
234 | Aws::StringStream ss; |
235 | ss << EC2_SECURITY_CREDENTIALS_RESOURCE << "/" << securityCredentials[0]; |
236 | AWS_LOGSTREAM_DEBUG(m_logtag.c_str(), "Calling EC2MetadataService resource " << ss.str()); |
237 | return GetResource(ss.str().c_str()); |
238 | } |
239 | |
240 | Aws::String EC2MetadataClient::GetDefaultCredentialsSecurely() const |
241 | { |
242 | std::unique_lock<std::recursive_mutex> locker(m_tokenMutex); |
243 | if (!m_tokenRequired) |
244 | { |
245 | return GetDefaultCredentials(); |
246 | } |
247 | |
248 | Aws::StringStream ss; |
249 | ss << m_endpoint << EC2_IMDS_TOKEN_RESOURCE; |
250 | std::shared_ptr<HttpRequest> tokenRequest(CreateHttpRequest(ss.str(), HttpMethod::HTTP_PUT, |
251 | Aws::Utils::Stream::DefaultResponseStreamFactoryMethod)); |
252 | tokenRequest->SetHeaderValue(EC2_IMDS_TOKEN_TTL_HEADER, EC2_IMDS_TOKEN_TTL_DEFAULT_VALUE); |
253 | auto userAgentString = ComputeUserAgentString(); |
254 | tokenRequest->SetUserAgent(userAgentString); |
255 | AWS_LOGSTREAM_TRACE(m_logtag.c_str(), "Calling EC2MetadataService to get token" ); |
256 | auto result = GetResourceWithAWSWebServiceResult(tokenRequest); |
257 | Aws::String tokenString = result.GetPayload(); |
258 | Aws::String trimmedTokenString = StringUtils::Trim(tokenString.c_str()); |
259 | |
260 | if (result.GetResponseCode() == HttpResponseCode::BAD_REQUEST) |
261 | { |
262 | return {}; |
263 | } |
264 | else if (result.GetResponseCode() != HttpResponseCode::OK || trimmedTokenString.empty()) |
265 | { |
266 | m_tokenRequired = false; |
267 | AWS_LOGSTREAM_TRACE(m_logtag.c_str(), "Calling EC2MetadataService to get token failed, falling back to less secure way." ); |
268 | return GetDefaultCredentials(); |
269 | } |
270 | m_token = trimmedTokenString; |
271 | locker.unlock(); |
272 | ss.str("" ); |
273 | ss << m_endpoint << EC2_SECURITY_CREDENTIALS_RESOURCE; |
274 | std::shared_ptr<HttpRequest> profileRequest(CreateHttpRequest(ss.str(), HttpMethod::HTTP_GET, |
275 | Aws::Utils::Stream::DefaultResponseStreamFactoryMethod)); |
276 | profileRequest->SetHeaderValue(EC2_IMDS_TOKEN_HEADER, trimmedTokenString); |
277 | profileRequest->SetUserAgent(userAgentString); |
278 | Aws::String profileString = GetResourceWithAWSWebServiceResult(profileRequest).GetPayload(); |
279 | |
280 | Aws::String trimmedProfileString = StringUtils::Trim(profileString.c_str()); |
281 | Aws::Vector<Aws::String> securityCredentials = StringUtils::Split(trimmedProfileString, '\n'); |
282 | |
283 | AWS_LOGSTREAM_DEBUG(m_logtag.c_str(), "Calling EC2MetadataService resource, " << EC2_SECURITY_CREDENTIALS_RESOURCE |
284 | << " with token returned profile string " << trimmedProfileString); |
285 | if (securityCredentials.size() == 0) |
286 | { |
287 | AWS_LOGSTREAM_WARN(m_logtag.c_str(), "Calling EC2Metadataservice to get profiles failed" ); |
288 | return {}; |
289 | } |
290 | |
291 | ss.str("" ); |
292 | ss << m_endpoint << EC2_SECURITY_CREDENTIALS_RESOURCE << "/" << securityCredentials[0]; |
293 | std::shared_ptr<HttpRequest> credentialsRequest(CreateHttpRequest(ss.str(), HttpMethod::HTTP_GET, |
294 | Aws::Utils::Stream::DefaultResponseStreamFactoryMethod)); |
295 | credentialsRequest->SetHeaderValue(EC2_IMDS_TOKEN_HEADER, trimmedTokenString); |
296 | credentialsRequest->SetUserAgent(userAgentString); |
297 | AWS_LOGSTREAM_DEBUG(m_logtag.c_str(), "Calling EC2MetadataService resource " << ss.str() << " with token." ); |
298 | return GetResourceWithAWSWebServiceResult(credentialsRequest).GetPayload(); |
299 | } |
300 | |
301 | Aws::String EC2MetadataClient::GetCurrentRegion() const |
302 | { |
303 | AWS_LOGSTREAM_TRACE(m_logtag.c_str(), "Getting current region for ec2 instance" ); |
304 | |
305 | Aws::StringStream ss; |
306 | ss << m_endpoint << EC2_REGION_RESOURCE; |
307 | std::shared_ptr<HttpRequest> regionRequest(CreateHttpRequest(ss.str(), HttpMethod::HTTP_GET, |
308 | Aws::Utils::Stream::DefaultResponseStreamFactoryMethod)); |
309 | { |
310 | std::lock_guard<std::recursive_mutex> locker(m_tokenMutex); |
311 | if (m_tokenRequired) |
312 | { |
313 | regionRequest->SetHeaderValue(EC2_IMDS_TOKEN_HEADER, m_token); |
314 | } |
315 | } |
316 | regionRequest->SetUserAgent(ComputeUserAgentString()); |
317 | Aws::String azString = GetResourceWithAWSWebServiceResult(regionRequest).GetPayload(); |
318 | |
319 | if (azString.empty()) |
320 | { |
321 | AWS_LOGSTREAM_INFO(m_logtag.c_str() , |
322 | "Unable to pull region from instance metadata service " ); |
323 | return {}; |
324 | } |
325 | |
326 | Aws::String trimmedAZString = StringUtils::Trim(azString.c_str()); |
327 | AWS_LOGSTREAM_DEBUG(m_logtag.c_str(), "Calling EC2MetadataService resource " |
328 | << EC2_REGION_RESOURCE << " , returned credential string " << trimmedAZString); |
329 | |
330 | Aws::String region; |
331 | region.reserve(trimmedAZString.length()); |
332 | |
333 | bool digitFound = false; |
334 | for (auto character : trimmedAZString) |
335 | { |
336 | if(digitFound && !isdigit(character)) |
337 | { |
338 | break; |
339 | } |
340 | if (isdigit(character)) |
341 | { |
342 | digitFound = true; |
343 | } |
344 | |
345 | region.append(1, character); |
346 | } |
347 | |
348 | AWS_LOGSTREAM_INFO(m_logtag.c_str(), "Detected current region as " << region); |
349 | return region; |
350 | } |
351 | |
352 | ECSCredentialsClient::ECSCredentialsClient(const char* resourcePath, const char* endpoint, const char* token) |
353 | : AWSHttpResourceClient(ECS_CREDENTIALS_CLIENT_LOG_TAG), |
354 | m_resourcePath(resourcePath), m_endpoint(endpoint), m_token(token) |
355 | { |
356 | } |
357 | |
358 | ECSCredentialsClient::ECSCredentialsClient(const Aws::Client::ClientConfiguration& clientConfiguration, const char* resourcePath, const char* endpoint, const char* token) |
359 | : AWSHttpResourceClient(clientConfiguration, ECS_CREDENTIALS_CLIENT_LOG_TAG), |
360 | m_resourcePath(resourcePath), m_endpoint(endpoint), m_token(token) |
361 | { |
362 | } |
363 | |
364 | static const char STS_RESOURCE_CLIENT_LOG_TAG[] = "STSResourceClient" ; |
365 | STSCredentialsClient::STSCredentialsClient(const Aws::Client::ClientConfiguration& clientConfiguration) |
366 | : AWSHttpResourceClient(clientConfiguration, STS_RESOURCE_CLIENT_LOG_TAG) |
367 | { |
368 | SetErrorMarshaller(Aws::MakeUnique<Aws::Client::XmlErrorMarshaller>(STS_RESOURCE_CLIENT_LOG_TAG)); |
369 | |
370 | Aws::StringStream ss; |
371 | if (clientConfiguration.scheme == Aws::Http::Scheme::HTTP) |
372 | { |
373 | ss << "http://" ; |
374 | } |
375 | else |
376 | { |
377 | ss << "https://" ; |
378 | } |
379 | |
380 | static const int CN_NORTH_1_HASH = Aws::Utils::HashingUtils::HashString(Aws::Region::CN_NORTH_1); |
381 | static const int CN_NORTHWEST_1_HASH = Aws::Utils::HashingUtils::HashString(Aws::Region::CN_NORTHWEST_1); |
382 | auto hash = Aws::Utils::HashingUtils::HashString(clientConfiguration.region.c_str()); |
383 | |
384 | ss << "sts." << clientConfiguration.region << ".amazonaws.com" ; |
385 | if (hash == CN_NORTH_1_HASH || hash == CN_NORTHWEST_1_HASH) |
386 | { |
387 | ss << ".cn" ; |
388 | } |
389 | m_endpoint = ss.str(); |
390 | |
391 | AWS_LOGSTREAM_INFO(STS_RESOURCE_CLIENT_LOG_TAG, "Creating STS ResourceClient with endpoint: " << m_endpoint); |
392 | } |
393 | |
394 | STSCredentialsClient::STSAssumeRoleWithWebIdentityResult STSCredentialsClient::GetAssumeRoleWithWebIdentityCredentials(const STSAssumeRoleWithWebIdentityRequest& request) |
395 | { |
396 | //Calculate query string |
397 | Aws::StringStream ss; |
398 | ss << "/?Action=AssumeRoleWithWebIdentity" |
399 | << "&Version=2011-06-15" |
400 | << "&RoleSessionName=" << Aws::Utils::StringUtils::URLEncode(request.roleSessionName.c_str()) |
401 | << "&RoleArn=" << Aws::Utils::StringUtils::URLEncode(request.roleArn.c_str()) |
402 | << "&WebIdentityToken=" << Aws::Utils::StringUtils::URLEncode(request.webIdentityToken.c_str()); |
403 | |
404 | Aws::String credentialsStr = GetResource(m_endpoint.c_str(), ss.str().c_str()/*query string*/, nullptr/*no auth token needed*/); |
405 | |
406 | //Parse credentials |
407 | STSAssumeRoleWithWebIdentityResult result; |
408 | if (credentialsStr.empty()) |
409 | { |
410 | AWS_LOGSTREAM_WARN(STS_RESOURCE_CLIENT_LOG_TAG, "Get an empty credential from sts" ); |
411 | return result; |
412 | } |
413 | |
414 | const Utils::Xml::XmlDocument xmlDocument = XmlDocument::CreateFromXmlString(credentialsStr); |
415 | XmlNode rootNode = xmlDocument.GetRootElement(); |
416 | XmlNode resultNode = rootNode; |
417 | if (!rootNode.IsNull() && (rootNode.GetName() != "AssumeRoleWithWebIdentityResult" )) |
418 | { |
419 | resultNode = rootNode.FirstChild("AssumeRoleWithWebIdentityResult" ); |
420 | } |
421 | |
422 | if (!resultNode.IsNull()) |
423 | { |
424 | XmlNode credentialsNode = resultNode.FirstChild("Credentials" ); |
425 | if (!credentialsNode.IsNull()) |
426 | { |
427 | XmlNode accessKeyIdNode = credentialsNode.FirstChild("AccessKeyId" ); |
428 | if (!accessKeyIdNode.IsNull()) |
429 | { |
430 | result.creds.SetAWSAccessKeyId(accessKeyIdNode.GetText()); |
431 | } |
432 | |
433 | XmlNode secretAccessKeyNode = credentialsNode.FirstChild("SecretAccessKey" ); |
434 | if (!secretAccessKeyNode.IsNull()) |
435 | { |
436 | result.creds.SetAWSSecretKey(secretAccessKeyNode.GetText()); |
437 | } |
438 | |
439 | XmlNode sessionTokenNode = credentialsNode.FirstChild("SessionToken" ); |
440 | if (!sessionTokenNode.IsNull()) |
441 | { |
442 | result.creds.SetSessionToken(sessionTokenNode.GetText()); |
443 | } |
444 | |
445 | XmlNode expirationNode = credentialsNode.FirstChild("Expiration" ); |
446 | if (!expirationNode.IsNull()) |
447 | { |
448 | result.creds.SetExpiration(DateTime(StringUtils::Trim(expirationNode.GetText().c_str()).c_str(), DateFormat::ISO_8601)); |
449 | } |
450 | } |
451 | } |
452 | return result; |
453 | } |
454 | |