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/http/URI.h>
17
18#include <aws/core/utils/StringUtils.h>
19#include <aws/core/utils/memory/stl/AWSStringStream.h>
20#include <aws/core/utils/memory/stl/AWSSet.h>
21
22#include <cstdlib>
23#include <cctype>
24#include <cassert>
25#include <algorithm>
26#include <iomanip>
27
28using namespace Aws::Http;
29using namespace Aws::Utils;
30
31namespace Aws
32{
33namespace Http
34{
35
36const char* SEPARATOR = "://";
37
38} // namespace Http
39} // namespace Aws
40
41URI::URI() : m_scheme(Scheme::HTTP), m_port(HTTP_DEFAULT_PORT)
42{
43}
44
45URI::URI(const Aws::String& uri) : m_scheme(Scheme::HTTP), m_port(HTTP_DEFAULT_PORT)
46{
47 ParseURIParts(uri);
48}
49
50URI::URI(const char* uri) : m_scheme(Scheme::HTTP), m_port(HTTP_DEFAULT_PORT)
51{
52 ParseURIParts(uri);
53}
54
55URI& URI::operator =(const Aws::String& uri)
56{
57 this->ParseURIParts(uri);
58 return *this;
59}
60
61URI& URI::operator =(const char* uri)
62{
63 this->ParseURIParts(uri);
64 return *this;
65}
66
67bool URI::operator ==(const URI& other) const
68{
69 return CompareURIParts(other);
70}
71
72bool URI::operator ==(const Aws::String& other) const
73{
74 return CompareURIParts(other);
75}
76
77bool URI::operator ==(const char* other) const
78{
79 return CompareURIParts(other);
80}
81
82bool URI::operator !=(const URI& other) const
83{
84 return !(*this == other);
85}
86
87bool URI::operator !=(const Aws::String& other) const
88{
89 return !(*this == other);
90}
91
92bool URI::operator !=(const char* other) const
93{
94 return !(*this == other);
95}
96
97void URI::SetScheme(Scheme value)
98{
99 assert(value == Scheme::HTTP || value == Scheme::HTTPS);
100
101 if (value == Scheme::HTTP)
102 {
103 m_port = m_port == HTTPS_DEFAULT_PORT || m_port == 0 ? HTTP_DEFAULT_PORT : m_port;
104 m_scheme = value;
105 }
106 else if (value == Scheme::HTTPS)
107 {
108 m_port = m_port == HTTP_DEFAULT_PORT || m_port == 0 ? HTTPS_DEFAULT_PORT : m_port;
109 m_scheme = value;
110 }
111}
112
113Aws::String URI::URLEncodePathRFC3986(const Aws::String& path)
114{
115 if(path.empty())
116 {
117 return path;
118 }
119
120 const Aws::Vector<Aws::String> pathParts = StringUtils::Split(path, '/');
121 Aws::StringStream ss;
122 ss << std::hex << std::uppercase;
123
124 // escape characters appearing in a URL path according to RFC 3986
125 for (const auto& segment : pathParts)
126 {
127 ss << '/';
128 for(unsigned char c : segment) // alnum results in UB if the value of c is not unsigned char & is not EOF
129 {
130 // §2.3 unreserved characters
131 if (StringUtils::IsAlnum(c))
132 {
133 ss << c;
134 continue;
135 }
136 switch(c)
137 {
138 // §2.3 unreserved characters
139 case '-': case '_': case '.': case '~':
140 // The path section of the URL allow reserved characters to appear unescaped
141 // RFC 3986 §2.2 Reserved characters
142 // NOTE: this implementation does not accurately implement the RFC on purpose to accommodate for
143 // discrepancies in the implementations of URL encoding between AWS services for legacy reasons.
144 case '$': case '&': case ',':
145 case ':': case '=': case '@':
146 ss << c;
147 break;
148 default:
149 ss << '%' << std::setfill('0') << std::setw(2) << (int)((unsigned char)c) << std::setw(0);
150 }
151 }
152 }
153
154 //if the last character was also a slash, then add that back here.
155 if (path.back() == '/')
156 {
157 ss << '/';
158 }
159
160 return ss.str();
161}
162
163Aws::String URI::URLEncodePath(const Aws::String& path)
164{
165 Aws::Vector<Aws::String> pathParts = StringUtils::Split(path, '/');
166 Aws::StringStream ss;
167
168 for (Aws::Vector<Aws::String>::iterator iter = pathParts.begin(); iter != pathParts.end(); ++iter)
169 {
170 ss << '/' << StringUtils::URLEncode(iter->c_str());
171 }
172
173 //if the last character was also a slash, then add that back here.
174 if (path[path.length() - 1] == '/')
175 {
176 ss << '/';
177 }
178
179 return ss.str();
180}
181
182void URI::SetPath(const Aws::String& value)
183{
184 const Aws::Vector<Aws::String> pathParts = StringUtils::Split(value, '/');
185 Aws::String path;
186 path.reserve(value.length() + 1/* in case we have to append slash before the path. */);
187
188 for (const auto& segment : pathParts)
189 {
190 path.push_back('/');
191 path.append(segment);
192 }
193
194 if (value.back() == '/')
195 {
196 path.push_back('/');
197 }
198 m_path = std::move(path);
199}
200
201//ugh, this isn't even part of the canonicalization spec. It is part of how our services have implemented their signers though....
202//it doesn't really hurt anything to reorder it though, so go ahead and sort the values for parameters with the same key
203void InsertValueOrderedParameter(QueryStringParameterCollection& queryParams, const Aws::String& key, const Aws::String& value)
204{
205 auto entriesAtKey = queryParams.equal_range(key);
206 for (auto& entry = entriesAtKey.first; entry != entriesAtKey.second; ++entry)
207 {
208 if (entry->second > value)
209 {
210 queryParams.emplace_hint(entry, key, value);
211 return;
212 }
213 }
214
215 queryParams.emplace(key, value);
216}
217
218QueryStringParameterCollection URI::GetQueryStringParameters(bool decode) const
219{
220 Aws::String queryString = GetQueryString();
221
222 QueryStringParameterCollection parameterCollection;
223
224 //if we actually have a query string
225 if (queryString.size() > 0)
226 {
227 size_t currentPos = 1, locationOfNextDelimiter = 1;
228
229 //while we have params left to parse
230 while (currentPos < queryString.size())
231 {
232 //find next key/value pair
233 locationOfNextDelimiter = queryString.find('&', currentPos);
234
235 Aws::String keyValuePair;
236
237 //if this isn't the last parameter
238 if (locationOfNextDelimiter != Aws::String::npos)
239 {
240 keyValuePair = queryString.substr(currentPos, locationOfNextDelimiter - currentPos);
241 }
242 //if it is the last parameter
243 else
244 {
245 keyValuePair = queryString.substr(currentPos);
246 }
247
248 //split on =
249 size_t locationOfEquals = keyValuePair.find('=');
250 Aws::String key = keyValuePair.substr(0, locationOfEquals);
251 Aws::String value = keyValuePair.substr(locationOfEquals + 1);
252
253 if(decode)
254 {
255 InsertValueOrderedParameter(parameterCollection, StringUtils::URLDecode(key.c_str()), StringUtils::URLDecode(value.c_str()));
256 }
257 else
258 {
259 InsertValueOrderedParameter(parameterCollection, key, value);
260 }
261
262 currentPos += keyValuePair.size() + 1;
263 }
264 }
265
266 return parameterCollection;
267}
268
269void URI::CanonicalizeQueryString()
270{
271 QueryStringParameterCollection sortedParameters = GetQueryStringParameters(false);
272 Aws::StringStream queryStringStream;
273
274 bool first = true;
275
276 if(sortedParameters.size() > 0)
277 {
278 queryStringStream << "?";
279 }
280
281 if(m_queryString.find('=') != std::string::npos)
282 {
283 for (QueryStringParameterCollection::iterator iter = sortedParameters.begin();
284 iter != sortedParameters.end(); ++iter)
285 {
286 if (!first)
287 {
288 queryStringStream << "&";
289 }
290
291 first = false;
292 queryStringStream << iter->first.c_str() << "=" << iter->second.c_str();
293 }
294
295 m_queryString = queryStringStream.str();
296 }
297}
298
299void URI::AddQueryStringParameter(const char* key, const Aws::String& value)
300{
301 if (m_queryString.size() <= 0)
302 {
303 m_queryString.append("?");
304 }
305 else
306 {
307 m_queryString.append("&");
308 }
309
310 m_queryString.append(StringUtils::URLEncode(key) + "=" + StringUtils::URLEncode(value.c_str()));
311}
312
313void URI::AddQueryStringParameter(const Aws::Map<Aws::String, Aws::String>& queryStringPairs)
314{
315 for(const auto& entry: queryStringPairs)
316 {
317 AddQueryStringParameter(entry.first.c_str(), entry.second);
318 }
319}
320
321void URI::SetQueryString(const Aws::String& str)
322{
323 m_queryString = "";
324
325 if (str.empty()) return;
326
327 if (str.front() != '?')
328 {
329 m_queryString.append("?").append(str);
330 }
331 else
332 {
333 m_queryString = str;
334 }
335}
336
337Aws::String URI::GetURIString(bool includeQueryString) const
338{
339 assert(m_authority.size() > 0);
340
341 Aws::StringStream ss;
342 ss << SchemeMapper::ToString(m_scheme) << SEPARATOR << m_authority;
343
344 if (m_scheme == Scheme::HTTP && m_port != HTTP_DEFAULT_PORT)
345 {
346 ss << ":" << m_port;
347 }
348 else if (m_scheme == Scheme::HTTPS && m_port != HTTPS_DEFAULT_PORT)
349 {
350 ss << ":" << m_port;
351 }
352
353 if(m_path != "/")
354 {
355 ss << URLEncodePathRFC3986(m_path);
356 }
357
358 if(includeQueryString)
359 {
360 ss << m_queryString;
361 }
362
363 return ss.str();
364}
365
366void URI::ParseURIParts(const Aws::String& uri)
367{
368 ExtractAndSetScheme(uri);
369 ExtractAndSetAuthority(uri);
370 ExtractAndSetPort(uri);
371 ExtractAndSetPath(uri);
372 ExtractAndSetQueryString(uri);
373}
374
375void URI::ExtractAndSetScheme(const Aws::String& uri)
376{
377 size_t posOfSeparator = uri.find(SEPARATOR);
378
379 if (posOfSeparator != Aws::String::npos)
380 {
381 Aws::String schemePortion = uri.substr(0, posOfSeparator);
382 SetScheme(SchemeMapper::FromString(schemePortion.c_str()));
383 }
384 else
385 {
386 SetScheme(Scheme::HTTP);
387 }
388}
389
390void URI::ExtractAndSetAuthority(const Aws::String& uri)
391{
392 size_t authorityStart = uri.find(SEPARATOR);
393
394 if (authorityStart == Aws::String::npos)
395 {
396 authorityStart = 0;
397 }
398 else
399 {
400 authorityStart += 3;
401 }
402
403 size_t posOfEndOfAuthorityPort = uri.find(':', authorityStart);
404 size_t posOfEndOfAuthoritySlash = uri.find('/', authorityStart);
405 size_t posOfEndOfAuthorityQuery = uri.find('?', authorityStart);
406 size_t posEndOfAuthority = (std::min)({posOfEndOfAuthorityPort, posOfEndOfAuthoritySlash, posOfEndOfAuthorityQuery});
407 if (posEndOfAuthority == Aws::String::npos)
408 {
409 posEndOfAuthority = uri.length();
410 }
411
412 SetAuthority(uri.substr(authorityStart, posEndOfAuthority - authorityStart));
413}
414
415void URI::ExtractAndSetPort(const Aws::String& uri)
416{
417 size_t authorityStart = uri.find(SEPARATOR);
418
419 if(authorityStart == Aws::String::npos)
420 {
421 authorityStart = 0;
422 }
423 else
424 {
425 authorityStart += 3;
426 }
427
428 size_t positionOfPortDelimiter = uri.find(':', authorityStart);
429
430 bool hasPort = positionOfPortDelimiter != Aws::String::npos;
431
432 if ((uri.find('/', authorityStart) < positionOfPortDelimiter) || (uri.find('?', authorityStart) < positionOfPortDelimiter))
433 {
434 hasPort = false;
435 }
436
437 if (hasPort)
438 {
439 Aws::String strPort;
440
441 size_t i = positionOfPortDelimiter + 1;
442 char currentDigit = uri[i];
443
444 while (std::isdigit(currentDigit))
445 {
446 strPort += currentDigit;
447 currentDigit = uri[++i];
448 }
449
450 SetPort(static_cast<uint16_t>(atoi(strPort.c_str())));
451 }
452}
453
454void URI::ExtractAndSetPath(const Aws::String& uri)
455{
456 size_t authorityStart = uri.find(SEPARATOR);
457
458 if (authorityStart == Aws::String::npos)
459 {
460 authorityStart = 0;
461 }
462 else
463 {
464 authorityStart += 3;
465 }
466
467 size_t pathEnd = uri.find('?');
468
469 if (pathEnd == Aws::String::npos)
470 {
471 pathEnd = uri.length();
472 }
473
474 Aws::String authorityAndPath = uri.substr(authorityStart, pathEnd - authorityStart);
475
476 size_t pathStart = authorityAndPath.find('/');
477
478 if (pathStart != Aws::String::npos)
479 {
480 SetPath(authorityAndPath.substr(pathStart, pathEnd - pathStart));
481 }
482 else
483 {
484 SetPath("/");
485 }
486}
487
488void URI::ExtractAndSetQueryString(const Aws::String& uri)
489{
490 size_t queryStart = uri.find('?');
491
492 if (queryStart != Aws::String::npos)
493 {
494 m_queryString = uri.substr(queryStart);
495 }
496}
497
498Aws::String URI::GetFormParameters() const
499{
500 if(m_queryString.length() == 0)
501 {
502 return "";
503 }
504 else
505 {
506 return m_queryString.substr(1);
507 }
508}
509
510bool URI::CompareURIParts(const URI& other) const
511{
512 return m_scheme == other.m_scheme && m_authority == other.m_authority && m_path == other.m_path && m_queryString == other.m_queryString;
513}
514