2 * Copyright (C) 1996-2025 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
9 /* DEBUG: section 16 Cache Manager Objects */
12 #include "AccessLogEntry.h"
13 #include "base/TextException.h"
14 #include "CacheManager.h"
15 #include "comm/Connection.h"
16 #include "debug/Stream.h"
17 #include "error/ExceptionErrorDetail.h"
18 #include "errorpage.h"
20 #include "HttpHdrCc.h"
21 #include "HttpReply.h"
22 #include "HttpRequest.h"
23 #include "mgr/Action.h"
24 #include "mgr/ActionCreator.h"
25 #include "mgr/ActionPasswordList.h"
26 #include "mgr/ActionProfile.h"
27 #include "mgr/BasicActions.h"
28 #include "mgr/Command.h"
29 #include "mgr/Forwarder.h"
30 #include "mgr/FunAction.h"
31 #include "mgr/QueryParams.h"
32 #include "parser/Tokenizer.h"
34 #include "sbuf/Stream.h"
35 #include "sbuf/StringConvert.h"
36 #include "SquidConfig.h"
44 /// \ingroup CacheManagerInternal
45 #define MGR_PASSWD_SZ 128
48 CacheManager::registerProfile(const Mgr::ActionProfile::Pointer
&profile
)
50 Must(profile
!= nullptr);
51 if (!CacheManager::findAction(profile
->name
)) {
52 menu_
.push_back(profile
);
53 debugs(16, 3, "registered profile: " << *profile
);
55 debugs(16, 2, "skipped duplicate profile: " << *profile
);
60 \ingroup CacheManagerInternal
61 * Locates an action in the actions registry ActionsList.
62 \retval NULL if Action not found
63 \retval CacheManagerAction* if the action was found
65 Mgr::ActionProfile::Pointer
66 CacheManager::findAction(char const * action
) const
68 Must(action
!= nullptr);
69 Menu::const_iterator a
;
71 debugs(16, 5, "CacheManager::findAction: looking for action " << action
);
72 for (a
= menu_
.begin(); a
!= menu_
.end(); ++a
) {
73 if (0 == strcmp((*a
)->name
, action
)) {
74 debugs(16, 6, " found");
79 debugs(16, 6, "Action not found.");
80 return Mgr::ActionProfilePointer();
84 CacheManager::createNamedAction(const char *actionName
)
88 Mgr::Command::Pointer cmd
= new Mgr::Command
;
89 cmd
->profile
= findAction(actionName
);
90 cmd
->params
.actionName
= actionName
;
92 Must(cmd
->profile
!= nullptr);
93 return cmd
->profile
->creator
->create(cmd
);
97 CacheManager::createRequestedAction(const Mgr::ActionParams
¶ms
)
99 Mgr::Command::Pointer cmd
= new Mgr::Command
;
100 cmd
->params
= params
;
101 cmd
->profile
= findAction(params
.actionName
.termedBuf());
102 Must(cmd
->profile
!= nullptr);
103 return cmd
->profile
->creator
->create(cmd
);
107 CacheManager::WellKnownUrlPathPrefix()
109 static const SBuf
prefix("/squid-internal-mgr/");
114 * Parses the action requested by the user and checks via
115 * CacheManager::ActionProtection() that the item is accessible by the user.
119 * [ scheme "://" authority ] '/squid-internal-mgr' path-absolute [ "?" query ] [ "#" fragment ]
121 * see RFC 3986 for definitions of scheme, authority, path-absolute, query
123 * \returns Mgr::Command object with action to perform and parameters it might use
125 Mgr::Command::Pointer
126 CacheManager::ParseUrl(const AnyP::Uri
&uri
)
128 Parser::Tokenizer
tok(uri
.path());
130 Assure(tok
.skip(WellKnownUrlPathPrefix()));
132 Mgr::Command::Pointer cmd
= new Mgr::Command();
133 cmd
->params
.httpUri
= SBufToString(uri
.absolute());
135 static const auto fieldChars
= CharacterSet("mgr-field", "?#").complement();
138 if (!tok
.prefix(action
, fieldChars
)) {
139 static const SBuf
indexReport("index");
140 action
= indexReport
;
142 cmd
->params
.actionName
= SBufToString(action
);
144 const auto profile
= findAction(action
.c_str());
146 throw TextException(ToSBuf("action '", action
, "' not found"), Here());
148 const char *prot
= ActionProtection(profile
);
149 if (!strcmp(prot
, "disabled") || !strcmp(prot
, "hidden"))
150 throw TextException(ToSBuf("action '", action
, "' is ", prot
), Here());
151 cmd
->profile
= profile
;
153 // TODO: fix when AnyP::Uri::parse() separates path?query#fragment
156 params
= tok
.remaining();
157 Mgr::QueryParams::Parse(tok
, cmd
->params
.queryParams
);
160 if (!tok
.skip('#') && !tok
.atEnd())
161 throw TextException("invalid characters in URL", Here());
162 // else ignore #fragment (if any)
164 debugs(16, 3, "MGR request: host=" << uri
.host() << ", action=" << action
<< ", params=" << params
);
169 /// \ingroup CacheManagerInternal
171 \ingroup CacheManagerInternal
172 * Decodes the headers needed to perform user authentication and fills
173 * the details into the cachemgrStateData argument
176 CacheManager::ParseHeaders(const HttpRequest
* request
, Mgr::ActionParams
¶ms
)
180 params
.httpMethod
= request
->method
.id();
181 params
.httpFlags
= request
->flags
;
183 #if HAVE_AUTH_MODULE_BASIC
184 // TODO: use the authentication system decode to retrieve these details properly.
186 /* base 64 _decoded_ user:passwd pair */
187 const auto basic_cookie(request
->header
.getAuthToken(Http::HdrType::AUTHORIZATION
, "Basic"));
189 if (basic_cookie
.isEmpty())
192 const auto colonPos
= basic_cookie
.find(':');
193 if (colonPos
== SBuf::npos
) {
194 debugs(16, DBG_IMPORTANT
, "ERROR: CacheManager::ParseHeaders: unknown basic_cookie format '" << basic_cookie
<< "'");
198 /* found user:password pair, reset old values */
199 params
.userName
= SBufToString(basic_cookie
.substr(0, colonPos
));
200 params
.password
= SBufToString(basic_cookie
.substr(colonPos
+1));
202 /* warning: this prints decoded password which maybe not be what you want to do @?@ @?@ */
203 debugs(16, 9, "CacheManager::ParseHeaders: got user: '" <<
204 params
.userName
<< "' passwd: '" << params
.password
<< "'");
209 \ingroup CacheManagerInternal
211 \retval 0 if mgr->password is good or "none"
212 \retval 1 if mgr->password is "disable"
213 \retval !0 if mgr->password does not match configured password
216 CacheManager::CheckPassword(const Mgr::Command
&cmd
)
218 assert(cmd
.profile
!= nullptr);
219 const char *action
= cmd
.profile
->name
;
220 char *pwd
= PasswdGet(Config
.passwd_list
, action
);
222 debugs(16, 4, "CacheManager::CheckPassword for action " << action
);
225 return cmd
.profile
->isPwReq
;
227 if (strcmp(pwd
, "disable") == 0)
230 if (strcmp(pwd
, "none") == 0)
233 if (!cmd
.params
.password
.size())
236 return cmd
.params
.password
!= pwd
;
240 \ingroup CacheManagerAPI
241 * Main entry point in the Cache Manager's activity. Gets called as part
242 * of the forward chain if the right URL is detected there. Initiates
243 * all needed internal work and renders the response.
246 CacheManager::start(const Comm::ConnectionPointer
&client
, HttpRequest
*request
, StoreEntry
*entry
, const AccessLogEntry::Pointer
&ale
)
248 debugs(16, 3, "request-url= '" << request
->url
<< "', entry-url='" << entry
->url() << "'");
250 Mgr::Command::Pointer cmd
;
252 cmd
= ParseUrl(request
->url
);
255 debugs(16, 2, "request URL error: " << CurrentException
);
256 const auto err
= new ErrorState(ERR_INVALID_URL
, Http::scNotFound
, request
, ale
);
257 err
->url
= xstrdup(entry
->url());
258 err
->detailError(new ExceptionErrorDetail(Here().id()));
259 errorAppendEntry(entry
, err
);
263 const char *actionName
= cmd
->profile
->name
;
265 entry
->expires
= squid_curtime
;
267 debugs(16, 5, "CacheManager: " << client
<< " requesting '" << actionName
<< "'");
269 /* get additional info from request headers */
270 ParseHeaders(request
, cmd
->params
);
272 const char *userName
= cmd
->params
.userName
.size() ?
273 cmd
->params
.userName
.termedBuf() : "unknown";
277 if (CheckPassword(*cmd
) != 0) {
278 /* build error message */
279 ErrorState
errState(ERR_CACHE_MGR_ACCESS_DENIED
, Http::scUnauthorized
, request
, ale
);
280 /* warn if user specified incorrect password */
282 if (cmd
->params
.password
.size()) {
283 debugs(16, DBG_IMPORTANT
, "CacheManager: " <<
285 client
<< ": incorrect password for '" <<
288 debugs(16, DBG_IMPORTANT
, "CacheManager: " <<
290 client
<< ": password needed for '" <<
294 HttpReply
*rep
= errState
.BuildHttpReply();
296 #if HAVE_AUTH_MODULE_BASIC
298 * add Authenticate header using action name as a realm because
299 * password depends on the action
301 rep
->header
.putAuth("Basic", actionName
);
304 const auto originOrNil
= request
->header
.getStr(Http::HdrType::ORIGIN
);
305 PutCommonResponseHeaders(*rep
, originOrNil
);
307 /* store the reply */
308 entry
->replaceHttpReply(rep
);
310 entry
->expires
= squid_curtime
;
317 if (request
->header
.has(Http::HdrType::ORIGIN
)) {
318 cmd
->params
.httpOrigin
= request
->header
.getStr(Http::HdrType::ORIGIN
);
321 debugs(16, 2, "CacheManager: " <<
323 client
<< " requesting '" <<
326 // special case: an index page
327 if (!strcmp(cmd
->profile
->name
, "index")) {
328 ErrorState
err(MGR_INDEX
, Http::scOkay
, request
, ale
);
329 err
.url
= xstrdup(entry
->url());
330 HttpReply
*rep
= err
.BuildHttpReply();
331 if (strncmp(rep
->body
.content(),"Internal Error:", 15) == 0)
332 rep
->sline
.set(Http::ProtocolVersion(1,1), Http::scNotFound
);
334 const auto originOrNil
= request
->header
.getStr(Http::HdrType::ORIGIN
);
335 PutCommonResponseHeaders(*rep
, originOrNil
);
337 entry
->replaceHttpReply(rep
);
342 if (UsingSmp() && IamWorkerProcess()) {
343 // is client the right connection to pass here?
344 AsyncJob::Start(new Mgr::Forwarder(client
, cmd
->params
, request
, entry
, ale
));
348 Mgr::Action::Pointer action
= cmd
->profile
->creator
->create(cmd
);
349 Must(action
!= nullptr);
350 action
->run(entry
, true);
354 \ingroup CacheManagerInternal
355 * Renders the protection level text for an action.
356 * Also doubles as a check for the protection level.
359 CacheManager::ActionProtection(const Mgr::ActionProfile::Pointer
&profile
)
361 assert(profile
!= nullptr);
362 const char *pwd
= PasswdGet(Config
.passwd_list
, profile
->name
);
365 return profile
->isPwReq
? "hidden" : "public";
367 if (!strcmp(pwd
, "disable"))
370 if (strcmp(pwd
, "none") == 0)
377 * \ingroup CacheManagerInternal
378 * gets from the global Config the password the user would need to supply
379 * for the action she queried
382 CacheManager::PasswdGet(Mgr::ActionPasswordList
* a
, const char *action
)
385 for (auto &w
: a
->actions
) {
386 if (w
.cmp(action
) == 0)
389 static const SBuf
allAction("all");
401 CacheManager::PutCommonResponseHeaders(HttpReply
&response
, const char *httpOrigin
)
403 // Allow cachemgr and other XHR scripts access to our version string
405 response
.header
.putExt("Access-Control-Allow-Origin", httpOrigin
);
406 #if HAVE_AUTH_MODULE_BASIC
407 response
.header
.putExt("Access-Control-Allow-Credentials", "true");
409 response
.header
.putExt("Access-Control-Expose-Headers", "Server");
413 // this is honored by more caches but allows pointless revalidation;
414 // revalidation will always fail because we do not support it (yet?)
415 cc
.noCache(String());
416 // this is honored by fewer caches but prohibits pointless revalidation
422 CacheManager::GetInstance()
424 static CacheManager
*instance
= nullptr;
426 debugs(16, 6, "starting cachemanager up");
427 instance
= new CacheManager
;
428 Mgr::RegisterBasics();