XRootD
Loading...
Searching...
No Matches
XrdClHttpHeaderBuilder.cc
Go to the documentation of this file.
2#include "XrdClHttpUtil.hh"
3
4#include <XrdCl/XrdClLog.hh>
6
8
9#include <algorithm>
10#include <array>
11#include <iterator>
12
13namespace {
14
15using namespace std::string_view_literals;
16
17// Headers the client must not inject: the RFC 7230 hop-by-hop and message
18// framing headers, and Host. Overriding one permits request smuggling, cache
19// poisoning, or corrupt framing.
20//
21// Held in lower case and without the TransferHeader prefix, the form
22// IsForbiddenHeader reduces a name to before it looks the name up.
23constexpr std::array forbidden_headers{
24 "connection"sv,
25 "content-length"sv,
26 "expect"sv,
27 "host"sv,
28 "keep-alive"sv,
29 "proxy-authenticate"sv,
30 "proxy-authorization"sv,
31 "proxy-connection"sv,
32 "te"sv,
33 "trailer"sv,
34 "transfer-encoding"sv,
35 "upgrade"sv
36};
37
38// The prefix that aims a header at the far server of a third party copy.
39constexpr std::string_view transfer_header_prefix{"transferheader"};
40
41// The characters RFC 7230 permits in a header name.
42constexpr std::string_view tchar{
43 "!#$%&'*+-.^_`|~"
44 "0123456789"
45 "abcdefghijklmnopqrstuvwxyz"
46 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
47};
48
49// The whitespace that may surround a header name or value.
50constexpr std::string_view ows{" \t"};
51
52// Returns `text` with any leading and trailing characters of `strip` removed.
53std::string_view trim(const std::string_view text, const std::string_view strip)
54{
55 const auto discard = [strip](const char c) {
56 return strip.find(c) != std::string_view::npos;
57 };
58
59 const auto first = std::find_if_not(text.begin(), text.end(), discard);
60 const auto last = std::find_if_not(text.rbegin(),
61 std::make_reverse_iterator(first), discard).base();
62
63 return text.substr(first - text.begin(), last - first);
64}
65
66} // namespace
67
68namespace XrdClHttp {
69namespace HeaderBuilder {
70
71bool
72IsForbiddenHeader(const std::string_view name)
73{
74 // A forbidden header stays forbidden when the TransferHeader prefix aims it
75 // at the far server of a third party copy, however often the prefix repeats.
76 std::string_view bare(name);
77 while (CompareIgnoreCase(bare.substr(0, transfer_header_prefix.size()), transfer_header_prefix)) {
78 bare.remove_prefix(transfer_header_prefix.size());
79 }
80
81 return std::find_if(forbidden_headers.begin(), forbidden_headers.end(), [bare](auto _sv) {
82 return CompareIgnoreCase(bare, _sv);
83 })
84 != forbidden_headers.end();
85}
86
87bool
88CompareIgnoreCase(const std::string_view lhs, const std::string_view rhs)
89{
90 const auto to_ascii_lower = [](const unsigned char c) {
91 return (c >= 'A' && c <= 'Z') ? static_cast<unsigned char>(c - 'A' + 'a') : c;
92 };
93
94 return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(),
95 [to_ascii_lower](const unsigned char a, const unsigned char b) {
96 return to_ascii_lower(a) == to_ascii_lower(b);
97 });
98}
99
100void
102{
103 for (const auto &header : extra) {
104 const auto present = std::any_of(headers.cbegin(), headers.cend(),
105 [&header](const auto &existing) {
106 return CompareIgnoreCase(existing.first, header.first);
107 });
108 if (!present) {
109 headers.emplace_back(header);
110 }
111 }
112}
113
114bool
115Build(const std::string_view spec, HeaderList &headers)
116{
118
119 headers.clear();
120
121 // Walk the newline separated entries, reporting every unusable one so a user
122 // who wrote several mistakes sees them all at once.
123 // An empty specification simply yields no headers.
124 HeaderList requested;
125 bool usable = true;
126 for (auto pos = spec.begin(); pos != spec.end(); ) {
127 const auto eol = std::find(pos, spec.end(), '\n');
128
129 // A CRLF separator leaves the CR behind; drop it along with any padding.
130 const auto entry = trim(spec.substr(pos - spec.begin(), eol - pos), " \t\r");
131 pos = (eol == spec.end() ? eol : std::next(eol));
132
133 // Tolerate blank entries so a trailing newline is not an error.
134 if (entry.empty()) {
135 continue;
136 }
137
138 // The value may itself contain colons, so split on the first one only.
139 // Without a colon the entry carries no value, so the log can show the
140 // whole entry without showing a value the user keeps private.
141 const auto colon = entry.find(':');
142 if (colon == std::string_view::npos) {
143 log->Error(kLogXrdClHttp, "Requested header %s holds no colon; each header must read \"<name>: <value>\"",
144 obfuscateAuth(std::string(entry)).c_str());
145 usable = false;
146 continue;
147 }
148
149 // The name must be a non-empty RFC 7230 token.
150 const std::string name(trim(entry.substr(0, colon), ows));
151 if (name.empty()) {
152 log->Error(kLogXrdClHttp, "Requested header holds no name before its colon");
153 usable = false;
154 continue;
155 }
156 if (!std::all_of(name.begin(), name.end(), [](const char c) {
157 return tchar.find(c) != std::string_view::npos;
158 }))
159 {
160 log->Error(kLogXrdClHttp, "Requested header %s holds a name that is not an HTTP token", name.c_str());
161 usable = false;
162 continue;
163 }
164 if (IsForbiddenHeader(name)) {
165 log->Error(kLogXrdClHttp, "Requested header %s must not be set by the client", name.c_str());
166 usable = false;
167 continue;
168 }
169
170 // A newline separates entries and so cannot occur in a value, but an
171 // embedded carriage return would let one forge part of the request.
172 const auto value = trim(entry.substr(colon + 1), ows);
173 if (value.empty()) {
174 log->Error(kLogXrdClHttp, "Requested header %s holds no value", name.c_str());
175 usable = false;
176 continue;
177 }
178 if (value.find('\r') != std::string_view::npos) {
179 log->Error(kLogXrdClHttp, "Requested header %s holds a carriage return in its value", name.c_str());
180 usable = false;
181 continue;
182 }
183
184 requested.emplace_back(name, value);
185 }
186
187 if (!usable) {
188 return false;
189 }
190
191 if (requested.empty()) {
192 return true;
193 }
194
195 // Name the headers but never their values, which the user fills with whatever
196 // the endpoint understands.
197 std::string names;
198 for (const auto &header : requested) {
199 if (!names.empty()) names += ", ";
200 names += header.first;
201 }
202 log->Debug(kLogXrdClHttp, "Requested headers %s", names.c_str());
203
204 headers = std::move(requested);
205 return true;
206}
207
208} // namespace HeaderBuilder
209
210} // namespace XrdClHttp
int extra
Definition XrdAccTest.cc:63
void trim(std::string &str)
Definition XrdHttpReq.cc:78
std::string obfuscateAuth(const std::string &input)
static Log * GetLog()
Get default log.
Handle diagnostics.
Definition XrdClLog.hh:101
void Error(uint64_t topic, const char *format,...)
Report an error.
Definition XrdClLog.cc:231
void Debug(uint64_t topic, const char *format,...)
Print a debug message.
Definition XrdClLog.cc:282
bool IsForbiddenHeader(const std::string_view name)
bool CompareIgnoreCase(const std::string_view lhs, const std::string_view rhs)
void AppendMissing(const HeaderList &extra, HeaderList &headers)
bool Build(const std::string_view spec, HeaderList &headers)
std::vector< std::pair< std::string, std::string > > HeaderList
const uint64_t kLogXrdClHttp