XRootD
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 
13 namespace {
14 
15 using 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.
23 constexpr 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.
39 constexpr std::string_view transfer_header_prefix{"transferheader"};
40 
41 // The characters RFC 7230 permits in a header name.
42 constexpr std::string_view tchar{
43  "!#$%&'*+-.^_`|~"
44  "0123456789"
45  "abcdefghijklmnopqrstuvwxyz"
46  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
47 };
48 
49 // The whitespace that may surround a header name or value.
50 constexpr std::string_view ows{" \t"};
51 
52 // Returns `text` with any leading and trailing characters of `strip` removed.
53 std::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 
68 namespace XrdClHttp {
69 namespace HeaderBuilder {
70 
71 bool
72 IsForbiddenHeader(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 
87 bool
88 CompareIgnoreCase(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 
100 void
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 
114 bool
115 Build(const std::string_view spec, HeaderList &headers)
116 {
117  XrdCl::Log *const log = XrdCl::DefaultEnv::GetLog();
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)
std::vector< std::pair< std::string, std::string > > HeaderList
bool Build(const std::string_view spec, HeaderList &headers)
const uint64_t kLogXrdClHttp