2 * Copyright (C) 1996-2021 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"
17 #include "error/ExceptionErrorDetail.h"
18 #include "errorpage.h"
20 #include "HttpReply.h"
21 #include "HttpRequest.h"
22 #include "mgr/Action.h"
23 #include "mgr/ActionCreator.h"
24 #include "mgr/ActionPasswordList.h"
25 #include "mgr/ActionProfile.h"
26 #include "mgr/BasicActions.h"
27 #include "mgr/Command.h"
28 #include "mgr/Forwarder.h"
29 #include "mgr/FunAction.h"
30 #include "mgr/QueryParams.h"
31 #include "parser/Tokenizer.h"
33 #include "sbuf/Stream.h"
34 #include "sbuf/StringConvert.h"
35 #include "SquidConfig.h"
36 #include "SquidTime.h"
43 /// \ingroup CacheManagerInternal
44 #define MGR_PASSWD_SZ 128
46 /// creates Action using supplied Action::Create method and command
47 class ClassActionCreator
: public Mgr::ActionCreator
50 typedef Mgr::Action::Pointer
Handler(const Mgr::Command::Pointer
&cmd
);
53 ClassActionCreator(Handler
*aHandler
): handler(aHandler
) {}
55 virtual Mgr::Action::Pointer
create(const Mgr::Command::Pointer
&cmd
) const {
63 /// Registers new profiles, ignoring attempts to register a duplicate
65 CacheManager::registerProfile(const Mgr::ActionProfile::Pointer
&profile
)
67 Must(profile
!= NULL
);
68 if (!CacheManager::findAction(profile
->name
)) {
69 menu_
.push_back(profile
);
70 debugs(16, 3, HERE
<< "registered profile: " << *profile
);
72 debugs(16, 2, HERE
<< "skipped duplicate profile: " << *profile
);
77 \ingroup CacheManagerAPI
78 * Registers a C-style action, which is implemented as a pointer to a function
79 * taking as argument a pointer to a StoreEntry and returning void.
80 * Implemented via CacheManagerActionLegacy.
83 CacheManager::registerProfile(char const * action
, char const * desc
, OBJH
* handler
, int pw_req_flag
, int atomic
)
85 debugs(16, 3, HERE
<< "registering legacy " << action
);
86 const Mgr::ActionProfile::Pointer profile
= new Mgr::ActionProfile(action
,
87 desc
, pw_req_flag
, atomic
, new Mgr::FunActionCreator(handler
));
88 registerProfile(profile
);
92 * \ingroup CacheManagerAPI
93 * Registers a C++-style action, via a pointer to a subclass of
94 * a CacheManagerAction object, whose run() method will be invoked when
95 * CacheManager identifies that the user has requested the action.
98 CacheManager::registerProfile(char const * action
, char const * desc
,
99 ClassActionCreator::Handler
*handler
,
100 int pw_req_flag
, int atomic
)
102 const Mgr::ActionProfile::Pointer profile
= new Mgr::ActionProfile(action
,
103 desc
, pw_req_flag
, atomic
, new ClassActionCreator(handler
));
104 registerProfile(profile
);
108 \ingroup CacheManagerInternal
109 * Locates an action in the actions registry ActionsList.
110 \retval NULL if Action not found
111 \retval CacheManagerAction* if the action was found
113 Mgr::ActionProfile::Pointer
114 CacheManager::findAction(char const * action
) const
116 Must(action
!= NULL
);
117 Menu::const_iterator a
;
119 debugs(16, 5, "CacheManager::findAction: looking for action " << action
);
120 for (a
= menu_
.begin(); a
!= menu_
.end(); ++a
) {
121 if (0 == strcmp((*a
)->name
, action
)) {
122 debugs(16, 6, " found");
127 debugs(16, 6, "Action not found.");
128 return Mgr::ActionProfilePointer();
132 CacheManager::createNamedAction(const char *actionName
)
136 Mgr::Command::Pointer cmd
= new Mgr::Command
;
137 cmd
->profile
= findAction(actionName
);
138 cmd
->params
.actionName
= actionName
;
140 Must(cmd
->profile
!= NULL
);
141 return cmd
->profile
->creator
->create(cmd
);
145 CacheManager::createRequestedAction(const Mgr::ActionParams
¶ms
)
147 Mgr::Command::Pointer cmd
= new Mgr::Command
;
148 cmd
->params
= params
;
149 cmd
->profile
= findAction(params
.actionName
.termedBuf());
150 Must(cmd
->profile
!= NULL
);
151 return cmd
->profile
->creator
->create(cmd
);
154 static const CharacterSet
&
155 MgrFieldChars(const AnyP::ProtocolType
&protocol
)
157 // Deprecated cache_object:// scheme used '@' to delimit passwords
158 if (protocol
== AnyP::PROTO_CACHE_OBJECT
) {
159 static const CharacterSet fieldChars
= CharacterSet("cache-object-field", "@?#").complement();
163 static const CharacterSet actionChars
= CharacterSet("mgr-field", "?#").complement();
168 * define whether the URL is a cache-manager URL and parse the action
169 * requested by the user. Checks via CacheManager::ActionProtection() that the
170 * item is accessible by the user.
174 * scheme "://" authority [ '/squid-internal-mgr' ] path-absolute [ '@' unreserved ] '?' query-string
176 * see RFC 3986 for definitions of scheme, authority, path-absolute, query-string
178 * \returns Mgr::Command object with action to perform and parameters it might use
180 Mgr::Command::Pointer
181 CacheManager::ParseUrl(const AnyP::Uri
&uri
)
183 Parser::Tokenizer
tok(uri
.path());
185 static const SBuf
internalMagicPrefix("/squid-internal-mgr/");
186 if (!tok
.skip(internalMagicPrefix
) && !tok
.skip('/'))
187 throw TextException("invalid URL path", Here());
189 Mgr::Command::Pointer cmd
= new Mgr::Command();
190 cmd
->params
.httpUri
= SBufToString(uri
.absolute());
192 const auto &fieldChars
= MgrFieldChars(uri
.getScheme());
195 if (!tok
.prefix(action
, fieldChars
)) {
196 if (uri
.getScheme() == AnyP::PROTO_CACHE_OBJECT
) {
197 static const SBuf
menuReport("menu");
200 static const SBuf
indexReport("index");
201 action
= indexReport
;
204 cmd
->params
.actionName
= SBufToString(action
);
206 const auto profile
= findAction(action
.c_str());
208 throw TextException(ToSBuf("action '", action
, "' not found"), Here());
210 const char *prot
= ActionProtection(profile
);
211 if (!strcmp(prot
, "disabled") || !strcmp(prot
, "hidden"))
212 throw TextException(ToSBuf("action '", action
, "' is ", prot
), Here());
213 cmd
->profile
= profile
;
216 if (uri
.getScheme() == AnyP::PROTO_CACHE_OBJECT
&& tok
.skip('@')) {
217 (void)tok
.prefix(passwd
, fieldChars
);
218 cmd
->params
.password
= SBufToString(passwd
);
221 // TODO: fix when AnyP::Uri::parse() separates path?query#fragment
224 params
= tok
.remaining();
225 Mgr::QueryParams::Parse(tok
, cmd
->params
.queryParams
);
228 if (!tok
.skip('#') && !tok
.atEnd())
229 throw TextException("invalid characters in URL", Here());
230 // else ignore #fragment (if any)
232 debugs(16, 3, "MGR request: host=" << uri
.host() << ", action=" << action
<<
233 ", password=" << passwd
<< ", params=" << params
);
238 /// \ingroup CacheManagerInternal
240 \ingroup CacheManagerInternal
241 * Decodes the headers needed to perform user authentication and fills
242 * the details into the cachemgrStateData argument
245 CacheManager::ParseHeaders(const HttpRequest
* request
, Mgr::ActionParams
¶ms
)
249 params
.httpMethod
= request
->method
.id();
250 params
.httpFlags
= request
->flags
;
252 #if HAVE_AUTH_MODULE_BASIC
253 // TODO: use the authentication system decode to retrieve these details properly.
255 /* base 64 _decoded_ user:passwd pair */
256 const auto basic_cookie(request
->header
.getAuthToken(Http::HdrType::AUTHORIZATION
, "Basic"));
258 if (basic_cookie
.isEmpty())
261 const auto colonPos
= basic_cookie
.find(':');
262 if (colonPos
== SBuf::npos
) {
263 debugs(16, DBG_IMPORTANT
, "CacheManager::ParseHeaders: unknown basic_cookie format '" << basic_cookie
<< "'");
267 /* found user:password pair, reset old values */
268 params
.userName
= SBufToString(basic_cookie
.substr(0, colonPos
));
269 params
.password
= SBufToString(basic_cookie
.substr(colonPos
+1));
271 /* warning: this prints decoded password which maybe not be what you want to do @?@ @?@ */
272 debugs(16, 9, "CacheManager::ParseHeaders: got user: '" <<
273 params
.userName
<< "' passwd: '" << params
.password
<< "'");
278 \ingroup CacheManagerInternal
280 \retval 0 if mgr->password is good or "none"
281 \retval 1 if mgr->password is "disable"
282 \retval !0 if mgr->password does not match configured password
285 CacheManager::CheckPassword(const Mgr::Command
&cmd
)
287 assert(cmd
.profile
!= NULL
);
288 const char *action
= cmd
.profile
->name
;
289 char *pwd
= PasswdGet(Config
.passwd_list
, action
);
291 debugs(16, 4, "CacheManager::CheckPassword for action " << action
);
294 return cmd
.profile
->isPwReq
;
296 if (strcmp(pwd
, "disable") == 0)
299 if (strcmp(pwd
, "none") == 0)
302 if (!cmd
.params
.password
.size())
305 return cmd
.params
.password
!= pwd
;
309 \ingroup CacheManagerAPI
310 * Main entry point in the Cache Manager's activity. Gets called as part
311 * of the forward chain if the right URL is detected there. Initiates
312 * all needed internal work and renders the response.
315 CacheManager::start(const Comm::ConnectionPointer
&client
, HttpRequest
*request
, StoreEntry
*entry
, const AccessLogEntry::Pointer
&ale
)
317 debugs(16, 3, "request-url= '" << request
->url
<< "', entry-url='" << entry
->url() << "'");
319 Mgr::Command::Pointer cmd
;
321 cmd
= ParseUrl(request
->url
);
324 debugs(16, 2, "request URL error: " << CurrentException
);
325 const auto err
= new ErrorState(ERR_INVALID_URL
, Http::scNotFound
, request
, ale
);
326 err
->url
= xstrdup(entry
->url());
327 err
->detailError(new ExceptionErrorDetail(Here().id()));
328 errorAppendEntry(entry
, err
);
329 entry
->expires
= squid_curtime
;
333 const char *actionName
= cmd
->profile
->name
;
335 entry
->expires
= squid_curtime
;
337 debugs(16, 5, "CacheManager: " << client
<< " requesting '" << actionName
<< "'");
339 /* get additional info from request headers */
340 ParseHeaders(request
, cmd
->params
);
342 const char *userName
= cmd
->params
.userName
.size() ?
343 cmd
->params
.userName
.termedBuf() : "unknown";
347 if (CheckPassword(*cmd
) != 0) {
348 /* build error message */
349 ErrorState
errState(ERR_CACHE_MGR_ACCESS_DENIED
, Http::scUnauthorized
, request
, ale
);
350 /* warn if user specified incorrect password */
352 if (cmd
->params
.password
.size()) {
353 debugs(16, DBG_IMPORTANT
, "CacheManager: " <<
355 client
<< ": incorrect password for '" <<
358 debugs(16, DBG_IMPORTANT
, "CacheManager: " <<
360 client
<< ": password needed for '" <<
364 HttpReply
*rep
= errState
.BuildHttpReply();
366 #if HAVE_AUTH_MODULE_BASIC
368 * add Authenticate header using action name as a realm because
369 * password depends on the action
371 rep
->header
.putAuth("Basic", actionName
);
373 // Allow cachemgr and other XHR scripts access to our version string
374 if (request
->header
.has(Http::HdrType::ORIGIN
)) {
375 rep
->header
.putExt("Access-Control-Allow-Origin",request
->header
.getStr(Http::HdrType::ORIGIN
));
376 #if HAVE_AUTH_MODULE_BASIC
377 rep
->header
.putExt("Access-Control-Allow-Credentials","true");
379 rep
->header
.putExt("Access-Control-Expose-Headers","Server");
382 /* store the reply */
383 entry
->replaceHttpReply(rep
);
385 entry
->expires
= squid_curtime
;
392 if (request
->header
.has(Http::HdrType::ORIGIN
)) {
393 cmd
->params
.httpOrigin
= request
->header
.getStr(Http::HdrType::ORIGIN
);
396 debugs(16, 2, "CacheManager: " <<
398 client
<< " requesting '" <<
401 // special case: /squid-internal-mgr/ index page
402 if (!strcmp(cmd
->profile
->name
, "index")) {
403 ErrorState
err(MGR_INDEX
, Http::scOkay
, request
, ale
);
404 err
.url
= xstrdup(entry
->url());
405 HttpReply
*rep
= err
.BuildHttpReply();
406 if (strncmp(rep
->body
.content(),"Internal Error:", 15) == 0)
407 rep
->sline
.set(Http::ProtocolVersion(1,1), Http::scNotFound
);
408 // Allow cachemgr and other XHR scripts access to our version string
409 if (request
->header
.has(Http::HdrType::ORIGIN
)) {
410 rep
->header
.putExt("Access-Control-Allow-Origin",request
->header
.getStr(Http::HdrType::ORIGIN
));
411 #if HAVE_AUTH_MODULE_BASIC
412 rep
->header
.putExt("Access-Control-Allow-Credentials","true");
414 rep
->header
.putExt("Access-Control-Expose-Headers","Server");
416 entry
->replaceHttpReply(rep
);
421 if (UsingSmp() && IamWorkerProcess()) {
422 // is client the right connection to pass here?
423 AsyncJob::Start(new Mgr::Forwarder(client
, cmd
->params
, request
, entry
, ale
));
427 Mgr::Action::Pointer action
= cmd
->profile
->creator
->create(cmd
);
428 Must(action
!= NULL
);
429 action
->run(entry
, true);
433 \ingroup CacheManagerInternal
434 * Renders the protection level text for an action.
435 * Also doubles as a check for the protection level.
438 CacheManager::ActionProtection(const Mgr::ActionProfile::Pointer
&profile
)
440 assert(profile
!= NULL
);
441 const char *pwd
= PasswdGet(Config
.passwd_list
, profile
->name
);
444 return profile
->isPwReq
? "hidden" : "public";
446 if (!strcmp(pwd
, "disable"))
449 if (strcmp(pwd
, "none") == 0)
456 * \ingroup CacheManagerInternal
457 * gets from the global Config the password the user would need to supply
458 * for the action she queried
461 CacheManager::PasswdGet(Mgr::ActionPasswordList
* a
, const char *action
)
464 for (auto &w
: a
->actions
) {
465 if (w
.cmp(action
) == 0)
468 static const SBuf
allAction("all");
480 CacheManager::GetInstance()
482 static CacheManager
*instance
= nullptr;
484 debugs(16, 6, "starting cachemanager up");
485 instance
= new CacheManager
;
486 Mgr::RegisterBasics();