XRootD
Loading...
Searching...
No Matches
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"
34#include "XrdSys/XrdSysError.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
46namespace
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
176bool 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
189void 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
199XrdXrootdRedirHelper::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
275bool 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}
static XrdSysError eDest(0,"crypto_")
void splitHostCgi(std::string_view target, std::string &host, std::string &cgi)
XrdSys::RAtomic< unsigned int > RAtomic_uint
const char * Set(const char *hSpec, int pNum=PortInSpec)
int Emsg(const char *esfx, int ecode, const char *text1, const char *text2=0)
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