XRootD
XrdXrootdRedirHelper.cc
Go to the documentation of this file.
1 /******************************************************************************/
2 /* */
3 /* X r d X r o o t d R e d i r H e l p e r . c c */
4 /* */
5 /* (c) 2026 by the Board of Trustees of the Leland Stanford, Jr., University */
6 /* */
7 /* This file is part of the XRootD software suite. */
8 /* */
9 /* XRootD is free software: you can redistribute it and/or modify it under */
10 /* the terms of the GNU Lesser General Public License as published by the */
11 /* Free Software Foundation, either version 3 of the License, or (at your */
12 /* option) any later version. */
13 /* */
14 /* XRootD is distributed in the hope that it will be useful, but WITHOUT */
15 /* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
16 /* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
17 /* License for more details. */
18 /* */
19 /* You should have received a copy of the GNU Lesser General Public License */
20 /* along with XRootD in a file called COPYING.LESSER (LGPL license) and file */
21 /* COPYING (GPL license). If not, see <http://www.gnu.org/licenses/>. */
22 /******************************************************************************/
23 
25 
26 #include <ctime>
27 #include <functional>
28 #include <map>
29 #include <memory>
30 
31 #include "XrdNet/XrdNetAddr.hh"
32 #include "XrdNet/XrdNetAddrInfo.hh"
34 #include "XrdSys/XrdSysError.hh"
35 #include "XrdSys/XrdSysPthread.hh"
36 #include "XrdSys/XrdSysRAtomic.hh"
38 
39 /******************************************************************************/
40 /* S t a t i c h e l p e r s t a t e */
41 /******************************************************************************/
42 
43 // All three globals below are written exactly once, by Init(), before any
44 // worker thread can observe them; they are therefore read without locking.
45 
46 namespace
47 {
48  XrdXrootdRedirPI *gPlugin = nullptr; // owned by the redirlib loader
49  XrdSysError *gLog = nullptr; // borrowed; logger lives forever
50  int gIPHold = 8 * 60 * 60;
51 
52  // ==========================================================================
53  // ====================== ONLY FOR TESTING, DO NOT MODIFY====================
54  // ==========================================================================
55  //
56  // Test-only clock seam. Production code leaves gNow empty, in which case
57  // Now() falls back to time(nullptr) on every call. The override is installed
58  // exclusively by XrdXrootdRedirHelper::SetClockForTesting() so unit tests can
59  // fast-forward past gIPHold and exercise the cache-refresh branch in
60  // LookupTarget.
61  //
62  // DO NOT call SetClockForTesting() from anywhere other than a test binary,
63  // and DO NOT add new code that reads or writes gNow directly. Replacing
64  // the wall-clock at runtime would silently break the netaddr cache TTL for
65  // every redirect in the whole process.
66  //
67  // ==========================================================================
68  std::function<time_t()> gNow;
69  inline time_t Now() { return gNow ? gNow() : time(nullptr); }
70 
71  //--------------------------------------------------------------------------
72  // Cached resolution of one redirect target. The cache is intentionally
73  // unbounded: a deployment has only a handful of redirect targets (one per
74  // data server) so the table never grows large enough to need eviction.
75  //--------------------------------------------------------------------------
76 
77  struct netInfo
78  {
79  XrdNetAddr netAddr;
80  XrdSysMutex niMutex;
81  std::string netID;
82  time_t expTime = 0;
83  RAtomic_uint refs = {0};
84 
85  explicit netInfo(const char *id) : netID(id) {}
86  };
87 
88  //--------------------------------------------------------------------------
89  // Scope guard that decrements netInfo::refs on destruction so that early
90  // returns (e.g. plugin error) do not leak the borrowed reference.
91  //--------------------------------------------------------------------------
92 
93  struct THandle
94  {
95  netInfo *Info = nullptr;
96  THandle() = default;
97  ~THandle() { if (Info) Info->refs--; }
98  THandle(const THandle&) = delete;
99  THandle& operator=(const THandle&) = delete;
100  };
101 
102  //--------------------------------------------------------------------------
107  //--------------------------------------------------------------------------
108 
109  netInfo *LookupTarget(const char *netID, int port)
110  {
111  static std::map<std::string, std::unique_ptr<netInfo>, std::less<>> niMap;
112  static XrdSysMutex niMapMtx;
113 
114  // Locate or insert the entry. Entries are immortal once created, so
115  // it is safe to drop the map lock as soon as we have the pointer.
116  niMapMtx.Lock();
117  netInfo *niP;
118  if (auto it = niMap.find(netID); it == niMap.end())
119  {auto newInfo = std::make_unique<netInfo>(netID);
120  niP = newInfo.get();
121  niMap.try_emplace(niP->netID, std::move(newInfo));
122  } else niP = it->second.get();
123  niMapMtx.UnLock();
124 
125  // Hold the per-entry mutex while we resolve / refresh the netaddr so
126  // that two callers do not race each other while the address is being
127  // populated.
128  niP->niMutex.Lock();
129  time_t nowT = Now();
130  niP->refs++;
131  // First-time init or expired entry: (re)resolve. refs == 1 means we
132  // are the only outstanding caller, so writing into netAddr cannot
133  // trample a concurrent reader; anyone else takes the cached address.
134  if (niP->expTime > nowT || niP->refs != 1)
135  {niP->niMutex.UnLock();
136  return niP;
137  }
138 
139  if (const char *eTxt = niP->netAddr.Set(netID, port); eTxt)
140  {if (niP->expTime == 0)
141  {// First-time resolution failed: drop the borrowed ref and
142  // tell the caller to fall back on the original target.
143  if (gLog) gLog->Emsg("RedirIP", "Unable to init NetInfo for",
144  netID, eTxt);
145  niP->refs--;
146  niP->niMutex.UnLock();
147  return nullptr;
148  }
149  // Refresh failed but we still have the previous good address;
150  // keep using it and back off the next refresh attempt.
151  if (gLog) gLog->Emsg("RedirIP", "Unable to refresh NetInfo for",
152  netID, eTxt);
153  niP->expTime += 60;
154  } else niP->expTime = nowT + gIPHold;
155  niP->niMutex.UnLock();
156  return niP;
157  }
158 }
159 
160 /******************************************************************************/
161 /* I n i t */
162 /******************************************************************************/
163 
165  int ipHold)
166 {
167  gPlugin = pi;
168  gLog = eDest;
169  gIPHold = ipHold;
170 }
171 
172 /******************************************************************************/
173 /* I s A c t i v e */
174 /******************************************************************************/
175 
176 bool XrdXrootdRedirHelper::IsActive() { return gPlugin != nullptr; }
177 
178 /******************************************************************************/
179 /* =================== ONLY FOR TESTING, DO NOT USE ==================== */
180 /* */
181 /* S e t C l o c k F o r T e s t i n g */
182 /* */
183 /* =================== ONLY FOR TESTING, DO NOT USE ==================== */
184 /******************************************************************************/
185 
186 // The only legal way to write gNow; see the header for full warnings.
187 // DO NOT CALL FROM PRODUCTION CODE.
188 
189 void XrdXrootdRedirHelper::SetClockForTesting(std::function<time_t()> nowFn)
190 {
191  gNow = std::move(nowFn);
192 }
193 
194 /******************************************************************************/
195 /* R e d i r e c t */
196 /******************************************************************************/
197 
199 XrdXrootdRedirHelper::Redirect(const char *trg, int &port,
200  XrdNetAddrInfo &clientAddr,
201  std::string &outTarget, std::string &errMsg)
202 {
203  if (!gPlugin || !trg) return Outcome::Unchanged;
204 
205  std::string pluginReply; // raw plugin reply, filled by the branch below
206  uint16_t hostPort = 0; // host form: the plugin's port (in/out)
207 
208  if (port >= 0)
209  {// Host[?cgi] form: split the target and invoke the plugin's host+port
210  // Redirect() entry point, which may rewrite the port in place.
211  if (port > UINT16_MAX)
212  {if (gLog) gLog->Emsg("RedirPI", "Redirect port out of range -",
213  std::to_string(port).c_str());
214  return Outcome::Unchanged;
215  }
216  std::string host;
217  std::string cgi;
218  splitHostCgi(trg, host, cgi);
219 
220  // The plugin call needs the target's resolved netaddr. If we cannot
221  // produce one the safest fallback is to leave the redirect alone
222  // (Unchanged) so the caller emits the original target unmodified.
223  THandle T;
224  T.Info = LookupTarget(host.c_str(), port);
225  if (!T.Info) return Outcome::Unchanged;
226 
227  hostPort = static_cast<uint16_t>(port);
228  pluginReply = gPlugin->Redirect(host.c_str(), hostPort, cgi.c_str(),
229  T.Info->netAddr, clientAddr);
230  } else {
231  // URL form: parse scheme://host[:port][/tail] and invoke the plugin's
232  // RedirectURL() entry point. port doubles as the rdrOpts argument and
233  // is the protocol's URL marker, so it is passed in but left unmodified.
234  // A URL we cannot parse is not salvageable: skip the plugin and report
235  // Unchanged so the caller emits the original target unmodified.
236  std::string urlHead;
237  std::string host;
238  std::string urlPort;
239  std::string urlTail;
240  if (!ParseURL(trg, urlHead, host, urlPort, urlTail))
241  {if (gLog) gLog->Emsg("RedirPI", "Invalid redirect URL -", trg);
242  return Outcome::Unchanged;
243  }
244 
245  // The cache is keyed by host alone (no scheme, no port suffix); pass
246  // -1 so the netaddr resolution does not bind to any specific port. A
247  // resolution failure falls back to Unchanged, as in the host form.
248  THandle T;
249  T.Info = LookupTarget(host.c_str(), -1);
250  if (!T.Info) return Outcome::Unchanged;
251 
252  int rdrOpts = port;
253  pluginReply = gPlugin->RedirectURL(urlHead.c_str(), host.c_str(),
254  urlPort.c_str(), urlTail.c_str(),
255  rdrOpts, T.Info->netAddr,
256  clientAddr);
257  }
258 
259  // Translate the plugin's "" / "<target>" / "!<msg>" return-string contract.
260  if (pluginReply.empty()) return Outcome::Unchanged;
261  if (pluginReply.front() == '!') { errMsg.assign(pluginReply, 1);
262  return Outcome::Error; }
263 
264  // Replaced: commit the plugin's (possibly rewritten) port. Host form only;
265  // the URL form leaves port untouched as the protocol's URL marker.
266  if (port >= 0) port = hostPort;
267  outTarget = std::move(pluginReply);
268  return Outcome::Replaced;
269 }
270 
271 /******************************************************************************/
272 /* P a r s e U R L */
273 /******************************************************************************/
274 
275 bool XrdXrootdRedirHelper::ParseURL(const char *url, std::string &urlHead,
276  std::string &host, std::string &port,
277  std::string &urlTail)
278 {
279  const char *hBeg = strstr(url, "://");
280  if (!hBeg) return false;
281  hBeg += 3;
282  urlHead.assign(url, hBeg - url);
283 
284  // Split off the path/query tail; require the host[:port] authority that
285  // precedes it to be at least two characters long.
286  if (const char *tail = strstr(hBeg, "/"); !tail)
287  {urlTail.clear(); host = hBeg;}
288  else {if (tail - hBeg < 3) return false;
289  host.assign(hBeg, tail - hBeg);
290  urlTail = tail;
291  }
292 
293  // Separate an optional ":port" suffix from the host.
294  port.clear();
295  if (size_t colon = host.find(':'); colon != std::string::npos)
296  {port.assign(host, colon + 1, std::string::npos);
297  host.erase(colon);
298  }
299  return true;
300 }
XrdSysError eDest(0, "HttpMon")
void splitHostCgi(std::string_view target, std::string &host, std::string &cgi)
int Emsg(const char *esfx, int ecode, const char *text1, const char *text2=0)
Definition: XrdSysError.cc:116
static Outcome Redirect(const char *trg, int &port, XrdNetAddrInfo &clientAddr, std::string &outTarget, std::string &errMsg)
static void Init(XrdXrootdRedirPI *pi, XrdSysError *eDest, int ipHold)
static bool ParseURL(const char *url, std::string &urlHead, std::string &host, std::string &port, std::string &urlTail)
static void SetClockForTesting(std::function< time_t()> nowFn)
virtual std::string RedirectURL(const char *urlHead, const char *Target, const char *port, const char *urlTail, int &rdrOpts, XrdNetAddrInfo &TNetInfo, XrdNetAddrInfo &CNetInfo)
virtual std::string Redirect(const char *Target, uint16_t &port, const char *TCgi, XrdNetAddrInfo &TNetInfo, XrdNetAddrInfo &CNetInfo)=0