]>
Commit | Line | Data |
---|---|---|
1 | /* | |
2 | * Copyright (C) 1996-2025 The Squid Software Foundation and contributors | |
3 | * | |
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. | |
7 | */ | |
8 | ||
9 | /* DEBUG: section 16 Cache Manager Objects */ | |
10 | ||
11 | #include "squid.h" | |
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" | |
19 | #include "fde.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" | |
33 | #include "protos.h" | |
34 | #include "sbuf/Stream.h" | |
35 | #include "sbuf/StringConvert.h" | |
36 | #include "SquidConfig.h" | |
37 | #include "Store.h" | |
38 | #include "tools.h" | |
39 | #include "wordlist.h" | |
40 | ||
41 | #include <algorithm> | |
42 | #include <memory> | |
43 | ||
44 | /// \ingroup CacheManagerInternal | |
45 | #define MGR_PASSWD_SZ 128 | |
46 | ||
47 | void | |
48 | CacheManager::registerProfile(const Mgr::ActionProfile::Pointer &profile) | |
49 | { | |
50 | Must(profile != nullptr); | |
51 | if (!CacheManager::findAction(profile->name)) { | |
52 | menu_.push_back(profile); | |
53 | debugs(16, 3, "registered profile: " << *profile); | |
54 | } else { | |
55 | debugs(16, 2, "skipped duplicate profile: " << *profile); | |
56 | } | |
57 | } | |
58 | ||
59 | /** | |
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 | |
64 | */ | |
65 | Mgr::ActionProfile::Pointer | |
66 | CacheManager::findAction(char const * action) const | |
67 | { | |
68 | Must(action != nullptr); | |
69 | Menu::const_iterator a; | |
70 | ||
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"); | |
75 | return *a; | |
76 | } | |
77 | } | |
78 | ||
79 | debugs(16, 6, "Action not found."); | |
80 | return Mgr::ActionProfilePointer(); | |
81 | } | |
82 | ||
83 | Mgr::Action::Pointer | |
84 | CacheManager::createNamedAction(const char *actionName) | |
85 | { | |
86 | Must(actionName); | |
87 | ||
88 | Mgr::Command::Pointer cmd = new Mgr::Command; | |
89 | cmd->profile = findAction(actionName); | |
90 | cmd->params.actionName = actionName; | |
91 | ||
92 | Must(cmd->profile != nullptr); | |
93 | return cmd->profile->creator->create(cmd); | |
94 | } | |
95 | ||
96 | Mgr::Action::Pointer | |
97 | CacheManager::createRequestedAction(const Mgr::ActionParams ¶ms) | |
98 | { | |
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); | |
104 | } | |
105 | ||
106 | const SBuf & | |
107 | CacheManager::WellKnownUrlPathPrefix() | |
108 | { | |
109 | static const SBuf prefix("/squid-internal-mgr/"); | |
110 | return prefix; | |
111 | } | |
112 | ||
113 | /** | |
114 | * Parses the action requested by the user and checks via | |
115 | * CacheManager::ActionProtection() that the item is accessible by the user. | |
116 | * | |
117 | * Syntax: | |
118 | * | |
119 | * [ scheme "://" authority ] '/squid-internal-mgr' path-absolute [ "?" query ] [ "#" fragment ] | |
120 | * | |
121 | * see RFC 3986 for definitions of scheme, authority, path-absolute, query | |
122 | * | |
123 | * \returns Mgr::Command object with action to perform and parameters it might use | |
124 | */ | |
125 | Mgr::Command::Pointer | |
126 | CacheManager::ParseUrl(const AnyP::Uri &uri) | |
127 | { | |
128 | Parser::Tokenizer tok(uri.path()); | |
129 | ||
130 | Assure(tok.skip(WellKnownUrlPathPrefix())); | |
131 | ||
132 | Mgr::Command::Pointer cmd = new Mgr::Command(); | |
133 | cmd->params.httpUri = SBufToString(uri.absolute()); | |
134 | ||
135 | static const auto fieldChars = CharacterSet("mgr-field", "?#").complement(); | |
136 | ||
137 | SBuf action; | |
138 | if (!tok.prefix(action, fieldChars)) { | |
139 | static const SBuf indexReport("index"); | |
140 | action = indexReport; | |
141 | } | |
142 | cmd->params.actionName = SBufToString(action); | |
143 | ||
144 | const auto profile = findAction(action.c_str()); | |
145 | if (!profile) | |
146 | throw TextException(ToSBuf("action '", action, "' not found"), Here()); | |
147 | ||
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; | |
152 | ||
153 | // TODO: fix when AnyP::Uri::parse() separates path?query#fragment | |
154 | SBuf params; | |
155 | if (tok.skip('?')) { | |
156 | params = tok.remaining(); | |
157 | Mgr::QueryParams::Parse(tok, cmd->params.queryParams); | |
158 | } | |
159 | ||
160 | if (!tok.skip('#') && !tok.atEnd()) | |
161 | throw TextException("invalid characters in URL", Here()); | |
162 | // else ignore #fragment (if any) | |
163 | ||
164 | debugs(16, 3, "MGR request: host=" << uri.host() << ", action=" << action << ", params=" << params); | |
165 | ||
166 | return cmd; | |
167 | } | |
168 | ||
169 | /// \ingroup CacheManagerInternal | |
170 | /* | |
171 | \ingroup CacheManagerInternal | |
172 | * Decodes the headers needed to perform user authentication and fills | |
173 | * the details into the cachemgrStateData argument | |
174 | */ | |
175 | void | |
176 | CacheManager::ParseHeaders(const HttpRequest * request, Mgr::ActionParams ¶ms) | |
177 | { | |
178 | assert(request); | |
179 | ||
180 | params.httpMethod = request->method.id(); | |
181 | params.httpFlags = request->flags; | |
182 | ||
183 | #if HAVE_AUTH_MODULE_BASIC | |
184 | // TODO: use the authentication system decode to retrieve these details properly. | |
185 | ||
186 | /* base 64 _decoded_ user:passwd pair */ | |
187 | const auto basic_cookie(request->header.getAuthToken(Http::HdrType::AUTHORIZATION, "Basic")); | |
188 | ||
189 | if (basic_cookie.isEmpty()) | |
190 | return; | |
191 | ||
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 << "'"); | |
195 | return; | |
196 | } | |
197 | ||
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)); | |
201 | ||
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 << "'"); | |
205 | #endif | |
206 | } | |
207 | ||
208 | /** | |
209 | \ingroup CacheManagerInternal | |
210 | * | |
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 | |
214 | */ | |
215 | int | |
216 | CacheManager::CheckPassword(const Mgr::Command &cmd) | |
217 | { | |
218 | assert(cmd.profile != nullptr); | |
219 | const char *action = cmd.profile->name; | |
220 | char *pwd = PasswdGet(Config.passwd_list, action); | |
221 | ||
222 | debugs(16, 4, "CacheManager::CheckPassword for action " << action); | |
223 | ||
224 | if (pwd == nullptr) | |
225 | return cmd.profile->isPwReq; | |
226 | ||
227 | if (strcmp(pwd, "disable") == 0) | |
228 | return 1; | |
229 | ||
230 | if (strcmp(pwd, "none") == 0) | |
231 | return 0; | |
232 | ||
233 | if (!cmd.params.password.size()) | |
234 | return 1; | |
235 | ||
236 | return cmd.params.password != pwd; | |
237 | } | |
238 | ||
239 | /** | |
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. | |
244 | */ | |
245 | void | |
246 | CacheManager::start(const Comm::ConnectionPointer &client, HttpRequest *request, StoreEntry *entry, const AccessLogEntry::Pointer &ale) | |
247 | { | |
248 | debugs(16, 3, "request-url= '" << request->url << "', entry-url='" << entry->url() << "'"); | |
249 | ||
250 | Mgr::Command::Pointer cmd; | |
251 | try { | |
252 | cmd = ParseUrl(request->url); | |
253 | ||
254 | } catch (...) { | |
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); | |
260 | return; | |
261 | } | |
262 | ||
263 | const char *actionName = cmd->profile->name; | |
264 | ||
265 | entry->expires = squid_curtime; | |
266 | ||
267 | debugs(16, 5, "CacheManager: " << client << " requesting '" << actionName << "'"); | |
268 | ||
269 | /* get additional info from request headers */ | |
270 | ParseHeaders(request, cmd->params); | |
271 | ||
272 | const char *userName = cmd->params.userName.size() ? | |
273 | cmd->params.userName.termedBuf() : "unknown"; | |
274 | ||
275 | /* Check password */ | |
276 | ||
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 */ | |
281 | ||
282 | if (cmd->params.password.size()) { | |
283 | debugs(16, DBG_IMPORTANT, "CacheManager: " << | |
284 | userName << "@" << | |
285 | client << ": incorrect password for '" << | |
286 | actionName << "'" ); | |
287 | } else { | |
288 | debugs(16, DBG_IMPORTANT, "CacheManager: " << | |
289 | userName << "@" << | |
290 | client << ": password needed for '" << | |
291 | actionName << "'" ); | |
292 | } | |
293 | ||
294 | HttpReply *rep = errState.BuildHttpReply(); | |
295 | ||
296 | #if HAVE_AUTH_MODULE_BASIC | |
297 | /* | |
298 | * add Authenticate header using action name as a realm because | |
299 | * password depends on the action | |
300 | */ | |
301 | rep->header.putAuth("Basic", actionName); | |
302 | #endif | |
303 | ||
304 | const auto originOrNil = request->header.getStr(Http::HdrType::ORIGIN); | |
305 | PutCommonResponseHeaders(*rep, originOrNil); | |
306 | ||
307 | /* store the reply */ | |
308 | entry->replaceHttpReply(rep); | |
309 | ||
310 | entry->expires = squid_curtime; | |
311 | ||
312 | entry->complete(); | |
313 | ||
314 | return; | |
315 | } | |
316 | ||
317 | if (request->header.has(Http::HdrType::ORIGIN)) { | |
318 | cmd->params.httpOrigin = request->header.getStr(Http::HdrType::ORIGIN); | |
319 | } | |
320 | ||
321 | debugs(16, 2, "CacheManager: " << | |
322 | userName << "@" << | |
323 | client << " requesting '" << | |
324 | actionName << "'" ); | |
325 | ||
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); | |
333 | ||
334 | const auto originOrNil = request->header.getStr(Http::HdrType::ORIGIN); | |
335 | PutCommonResponseHeaders(*rep, originOrNil); | |
336 | ||
337 | entry->replaceHttpReply(rep); | |
338 | entry->complete(); | |
339 | return; | |
340 | } | |
341 | ||
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)); | |
345 | return; | |
346 | } | |
347 | ||
348 | Mgr::Action::Pointer action = cmd->profile->creator->create(cmd); | |
349 | Must(action != nullptr); | |
350 | action->run(entry, true); | |
351 | } | |
352 | ||
353 | /* | |
354 | \ingroup CacheManagerInternal | |
355 | * Renders the protection level text for an action. | |
356 | * Also doubles as a check for the protection level. | |
357 | */ | |
358 | const char * | |
359 | CacheManager::ActionProtection(const Mgr::ActionProfile::Pointer &profile) | |
360 | { | |
361 | assert(profile != nullptr); | |
362 | const char *pwd = PasswdGet(Config.passwd_list, profile->name); | |
363 | ||
364 | if (!pwd) | |
365 | return profile->isPwReq ? "hidden" : "public"; | |
366 | ||
367 | if (!strcmp(pwd, "disable")) | |
368 | return "disabled"; | |
369 | ||
370 | if (strcmp(pwd, "none") == 0) | |
371 | return "public"; | |
372 | ||
373 | return "protected"; | |
374 | } | |
375 | ||
376 | /* | |
377 | * \ingroup CacheManagerInternal | |
378 | * gets from the global Config the password the user would need to supply | |
379 | * for the action she queried | |
380 | */ | |
381 | char * | |
382 | CacheManager::PasswdGet(Mgr::ActionPasswordList * a, const char *action) | |
383 | { | |
384 | while (a) { | |
385 | for (auto &w : a->actions) { | |
386 | if (w.cmp(action) == 0) | |
387 | return a->passwd; | |
388 | ||
389 | static const SBuf allAction("all"); | |
390 | if (w == allAction) | |
391 | return a->passwd; | |
392 | } | |
393 | ||
394 | a = a->next; | |
395 | } | |
396 | ||
397 | return nullptr; | |
398 | } | |
399 | ||
400 | void | |
401 | CacheManager::PutCommonResponseHeaders(HttpReply &response, const char *httpOrigin) | |
402 | { | |
403 | // Allow cachemgr and other XHR scripts access to our version string | |
404 | if (httpOrigin) { | |
405 | response.header.putExt("Access-Control-Allow-Origin", httpOrigin); | |
406 | #if HAVE_AUTH_MODULE_BASIC | |
407 | response.header.putExt("Access-Control-Allow-Credentials", "true"); | |
408 | #endif | |
409 | response.header.putExt("Access-Control-Expose-Headers", "Server"); | |
410 | } | |
411 | ||
412 | HttpHdrCc cc; | |
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 | |
417 | cc.noStore(true); | |
418 | response.putCc(cc); | |
419 | } | |
420 | ||
421 | CacheManager* | |
422 | CacheManager::GetInstance() | |
423 | { | |
424 | static CacheManager *instance = nullptr; | |
425 | if (!instance) { | |
426 | debugs(16, 6, "starting cachemanager up"); | |
427 | instance = new CacheManager; | |
428 | Mgr::RegisterBasics(); | |
429 | } | |
430 | return instance; | |
431 | } | |
432 |