1#include <userver/s3api/authenticators/signature_v4.hpp>
12#include <fmt/format.h>
14#include <userver/clients/http/request.hpp>
15#include <userver/crypto/aws.hpp>
16#include <userver/http/common_headers.hpp>
17#include <userver/s3api/models/request.hpp>
19USERVER_NAMESPACE_BEGIN
25std::string TrimAndCollapseSpaces(std::string_view value) {
27 result.reserve(value.size());
29 bool pending_space =
false;
30 for (
auto c : value) {
31 if (std::isspace(
static_cast<
unsigned char>(c))) {
32 pending_space = !result.empty();
37 result.push_back(
' ');
38 pending_space =
false;
47bool IsUnreservedChar(
char c) {
48 if ((c >=
'0' && c <=
'9') || (c >=
'A' && c <=
'Z') || (c >=
'a' && c <=
'z')) {
51 return c ==
'-' || c ==
'_' || c ==
'.' || c ==
'~';
54void PercentEncodeByteTo(
unsigned char byte, std::string& result) {
55 static constexpr char kHexDigits[] =
"0123456789ABCDEF";
56 result.push_back(
'%');
57 result.push_back(kHexDigits[byte >> 4]);
58 result.push_back(kHexDigits[byte & 0x0F]);
61std::optional<
int> ParseHexDigit(
char c) {
62 if (c >=
'0' && c <=
'9') {
65 if (c >=
'A' && c <=
'F') {
68 if (c >=
'a' && c <=
'f') {
74std::string UriEncode(std::string_view value,
bool encode_slash) {
76 result.reserve(value.size());
78 for (
auto c : value) {
79 if (IsUnreservedChar(c) || (c ==
'/' && !encode_slash)) {
82 PercentEncodeByteTo(
static_cast<
unsigned char>(c), result);
89std::string UriDecode(std::string_view value) {
91 result.reserve(value.size());
93 for (std::size_t i = 0; i < value.size(); ++i) {
94 if (value[i] ==
'%' && i + 2 < value.size()) {
95 const auto high = ParseHexDigit(value[i + 1]);
96 const auto low = ParseHexDigit(value[i + 2]);
98 result.push_back(
static_cast<
char>((*high * 16) + *low));
103 result.push_back(value[i]);
109struct RequestTarget {
110 std::string_view path;
111 std::string_view query;
114RequestTarget SplitRequestTarget(
const std::string& req) {
115 const std::string_view target{req};
116 const auto query_pos = target.find(
'?');
118 if (query_pos == std::string_view::npos) {
119 return RequestTarget{
125 return RequestTarget{
126 .path = target.substr(0, query_pos),
127 .query = target.substr(query_pos + 1),
131bool IsVirtualHostAddressing(std::string_view host, std::string_view bucket) {
132 if (bucket.empty()) {
135 if (host.size() <= bucket.size() || host[bucket.size()] !=
'.') {
138 return host.substr(0, bucket.size()) == bucket;
141std::string MakeCanonicalUri(
const Request& request, std::string_view host, std::string_view path) {
142 std::string raw_path;
144 if (!IsVirtualHostAddressing(host, request.bucket)) {
145 raw_path = request.bucket +
"/";
148 raw_path += UriDecode(path);
150 return "/" + UriEncode(raw_path,
false);
153using QueryParams = std::vector<std::pair<std::string, std::string>>;
155QueryParams ParseQuery(std::string_view query) {
158 while (!query.empty()) {
159 const auto param = query.substr(0, query.find(
'&'));
160 query.remove_prefix(std::min(query.size(), param.size() + 1));
166 const auto eq_pos = param.find(
'=');
167 if (eq_pos == std::string_view::npos) {
168 result.emplace_back(UriDecode(param), std::string{});
170 result.emplace_back(UriDecode(param.substr(0, eq_pos)), UriDecode(param.substr(eq_pos + 1)));
177std::string MakeCanonicalQueryString(QueryParams params) {
178 for (
auto& [name, value] : params) {
179 name = UriEncode(name,
true);
180 value = UriEncode(value,
true);
182 std::ranges::sort(params);
186 for (
const auto& [name, value] : params) {
187 if (!result.empty()) {
188 result.push_back(
'&');
191 result.push_back(
'=');
192 result.append(value);
198std::string GetHostHeaderValue(
const Request& request) {
199 const auto it = request.headers
.find(USERVER_NAMESPACE::http::
headers::kHost
);
200 if (it == request.headers
.end() || it->second.empty()) {
201 throw std::runtime_error(
"AWS Signature V4 requires the 'Host' header, set it before signing the request");
203 return TrimAndCollapseSpaces(it->second);
208std::unordered_map<std::string, std::string>
SignatureV4::Auth(
const Request& request)
const {
211 const auto host = GetHostHeaderValue(request);
212 const auto target = SplitRequestTarget(request.req);
213 const auto canonical_uri = MakeCanonicalUri(request, host, target.path);
214 const auto canonical_query = MakeCanonicalQueryString(ParseQuery(target.query));
216 auto headers = request.headers;
221 .canonical_uri = canonical_uri,
222 .canonical_query = canonical_query,
223 .payload = request.body,
224 .access_key = access_key_,
225 .secret_key = secret_key_.GetUnderlying(),
232 {
"Authorization", headers
[USERVER_NAMESPACE::http::
headers::kAuthorization
]},
233 {
"X-Amz-Date", headers
[crypto::
aws::kAmzDate
]},
234 {
"X-Amz-Content-Sha256", headers
[crypto::
aws::kAmzContentSha256
]},
242 const auto host = GetHostHeaderValue(request);
243 const auto target = SplitRequestTarget(request.req);
245 const auto expires_in = std::max<std::time_t>(expires - scope.now, 1);
247 std::unordered_map<std::string, std::string> sign_params{
249 {
"X-Amz-Credential",
fmt::format(
"{}/{}", access_key_, scope.credential_scope)},
250 {
"X-Amz-Date", scope.amz_date},
251 {
"X-Amz-Expires", std::to_string(expires_in)},
252 {
"X-Amz-SignedHeaders",
"host"},
255 QueryParams query_params = ParseQuery(target.query);
256 query_params.insert(query_params.end(), sign_params.begin(), sign_params.end());
260 MakeCanonicalUri(request, host, target.path)
,
261 MakeCanonicalQueryString(std::move(query_params))
,
262 fmt::format(
"host:{}\n", host)
,
264 crypto::
aws::kUnsignedPayload