]>
Commit | Line | Data |
---|---|---|
22f3fd98 | 1 | /* |
5b74111a | 2 | * Copyright (C) 1996-2018 The Squid Software Foundation and contributors |
e25c139f | 3 | * |
bbc27441 AJ |
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. | |
22f3fd98 | 7 | */ |
8 | ||
bbc27441 AJ |
9 | /* DEBUG: section 16 Cache Manager Objects */ |
10 | ||
f7f3304a | 11 | #include "squid.h" |
8822ebee | 12 | #include "base/TextException.h" |
62ee09ca | 13 | #include "CacheManager.h" |
5c336a3b | 14 | #include "comm/Connection.h" |
8822ebee | 15 | #include "Debug.h" |
aa839030 | 16 | #include "errorpage.h" |
8822ebee | 17 | #include "fde.h" |
528b2c61 | 18 | #include "HttpReply.h" |
19 | #include "HttpRequest.h" | |
8822ebee | 20 | #include "mgr/Action.h" |
602d9612 A |
21 | #include "mgr/ActionCreator.h" |
22 | #include "mgr/ActionPasswordList.h" | |
8822ebee AR |
23 | #include "mgr/ActionProfile.h" |
24 | #include "mgr/BasicActions.h" | |
25 | #include "mgr/Command.h" | |
26 | #include "mgr/Forwarder.h" | |
27 | #include "mgr/FunAction.h" | |
b8151fa1 | 28 | #include "mgr/QueryParams.h" |
592b8687 | 29 | #include "protos.h" |
4d5904f7 | 30 | #include "SquidConfig.h" |
985c86bc | 31 | #include "SquidTime.h" |
8822ebee | 32 | #include "Store.h" |
602d9612 | 33 | #include "tools.h" |
d295d770 | 34 | #include "wordlist.h" |
5bed43d6 | 35 | |
8822ebee | 36 | #include <algorithm> |
22f3fd98 | 37 | |
63be0a78 | 38 | /// \ingroup CacheManagerInternal |
22f3fd98 | 39 | #define MGR_PASSWD_SZ 128 |
40 | ||
8822ebee AR |
41 | /// creates Action using supplied Action::Create method and command |
42 | class ClassActionCreator: public Mgr::ActionCreator | |
43 | { | |
44 | public: | |
45 | typedef Mgr::Action::Pointer Handler(const Mgr::Command::Pointer &cmd); | |
c83f0bd5 | 46 | |
8822ebee AR |
47 | public: |
48 | ClassActionCreator(Handler *aHandler): handler(aHandler) {} | |
49 | ||
d9fc6862 | 50 | virtual Mgr::Action::Pointer create(const Mgr::Command::Pointer &cmd) const { |
8822ebee AR |
51 | return handler(cmd); |
52 | } | |
53 | ||
54 | private: | |
55 | Handler *handler; | |
56 | }; | |
57 | ||
8822ebee AR |
58 | /// Registers new profiles, ignoring attempts to register a duplicate |
59 | void | |
60 | CacheManager::registerProfile(const Mgr::ActionProfile::Pointer &profile) | |
62ee09ca | 61 | { |
8822ebee | 62 | Must(profile != NULL); |
e3c2ea01 | 63 | if (!CacheManager::findAction(profile->name)) { |
8822ebee AR |
64 | menu_.push_back(profile); |
65 | debugs(16, 3, HERE << "registered profile: " << *profile); | |
66 | } else { | |
67 | debugs(16, 2, HERE << "skipped duplicate profile: " << *profile); | |
68 | } | |
62ee09ca | 69 | } |
22f3fd98 | 70 | |
50847dca AJ |
71 | /** |
72 | \ingroup CacheManagerAPI | |
73 | * Registers a C-style action, which is implemented as a pointer to a function | |
74 | * taking as argument a pointer to a StoreEntry and returning void. | |
75 | * Implemented via CacheManagerActionLegacy. | |
76 | */ | |
22f3fd98 | 77 | void |
8822ebee | 78 | CacheManager::registerProfile(char const * action, char const * desc, OBJH * handler, int pw_req_flag, int atomic) |
22f3fd98 | 79 | { |
8822ebee AR |
80 | debugs(16, 3, HERE << "registering legacy " << action); |
81 | const Mgr::ActionProfile::Pointer profile = new Mgr::ActionProfile(action, | |
d9fc6862 | 82 | desc, pw_req_flag, atomic, new Mgr::FunActionCreator(handler)); |
8822ebee | 83 | registerProfile(profile); |
1d8395bd | 84 | } |
62e76326 | 85 | |
d154d3ec | 86 | /** |
e0d28505 | 87 | * \ingroup CacheManagerAPI |
a750e510 | 88 | * Registers a C++-style action, via a pointer to a subclass of |
d154d3ec FC |
89 | * a CacheManagerAction object, whose run() method will be invoked when |
90 | * CacheManager identifies that the user has requested the action. | |
91 | */ | |
1d8395bd | 92 | void |
8822ebee | 93 | CacheManager::registerProfile(char const * action, char const * desc, |
d9fc6862 A |
94 | ClassActionCreator::Handler *handler, |
95 | int pw_req_flag, int atomic) | |
1d8395bd | 96 | { |
8822ebee | 97 | const Mgr::ActionProfile::Pointer profile = new Mgr::ActionProfile(action, |
d9fc6862 | 98 | desc, pw_req_flag, atomic, new ClassActionCreator(handler)); |
8822ebee | 99 | registerProfile(profile); |
62ee09ca | 100 | } |
101 | ||
d154d3ec FC |
102 | /** |
103 | \ingroup CacheManagerInternal | |
104 | * Locates an action in the actions registry ActionsList. | |
105 | \retval NULL if Action not found | |
106 | \retval CacheManagerAction* if the action was found | |
107 | */ | |
8822ebee AR |
108 | Mgr::ActionProfile::Pointer |
109 | CacheManager::findAction(char const * action) const | |
22f3fd98 | 110 | { |
8822ebee AR |
111 | Must(action != NULL); |
112 | Menu::const_iterator a; | |
03c4599f K |
113 | |
114 | debugs(16, 5, "CacheManager::findAction: looking for action " << action); | |
8822ebee AR |
115 | for (a = menu_.begin(); a != menu_.end(); ++a) { |
116 | if (0 == strcmp((*a)->name, action)) { | |
03c4599f K |
117 | debugs(16, 6, " found"); |
118 | return *a; | |
119 | } | |
22f3fd98 | 120 | } |
62e76326 | 121 | |
03c4599f | 122 | debugs(16, 6, "Action not found."); |
8822ebee AR |
123 | return Mgr::ActionProfilePointer(); |
124 | } | |
125 | ||
126 | Mgr::Action::Pointer | |
127 | CacheManager::createNamedAction(const char *actionName) | |
128 | { | |
129 | Must(actionName); | |
130 | ||
131 | Mgr::Command::Pointer cmd = new Mgr::Command; | |
132 | cmd->profile = findAction(actionName); | |
133 | cmd->params.actionName = actionName; | |
134 | ||
135 | Must(cmd->profile != NULL); | |
136 | return cmd->profile->creator->create(cmd); | |
137 | } | |
138 | ||
139 | Mgr::Action::Pointer | |
140 | CacheManager::createRequestedAction(const Mgr::ActionParams ¶ms) | |
141 | { | |
142 | Mgr::Command::Pointer cmd = new Mgr::Command; | |
143 | cmd->params = params; | |
144 | cmd->profile = findAction(params.actionName.termedBuf()); | |
145 | Must(cmd->profile != NULL); | |
146 | return cmd->profile->creator->create(cmd); | |
22f3fd98 | 147 | } |
148 | ||
832c08ab FC |
149 | /** |
150 | \ingroup CacheManagerInternal | |
151 | * define whether the URL is a cache-manager URL and parse the action | |
152 | * requested by the user. Checks via CacheManager::ActionProtection() that the | |
153 | * item is accessible by the user. | |
154 | \retval CacheManager::cachemgrStateData state object for the following handling | |
155 | \retval NULL if the action can't be found or can't be accessed by the user | |
156 | */ | |
8822ebee | 157 | Mgr::Command::Pointer |
c83f0bd5 | 158 | CacheManager::ParseUrl(const char *url) |
22f3fd98 | 159 | { |
160 | int t; | |
161 | LOCAL_ARRAY(char, host, MAX_URL); | |
162 | LOCAL_ARRAY(char, request, MAX_URL); | |
163 | LOCAL_ARRAY(char, password, MAX_URL); | |
b8151fa1 CT |
164 | LOCAL_ARRAY(char, params, MAX_URL); |
165 | host[0] = 0; | |
166 | request[0] = 0; | |
167 | password[0] = 0; | |
168 | params[0] = 0; | |
169 | int pos = -1; | |
170 | int len = strlen(url); | |
171 | Must(len > 0); | |
172 | t = sscanf(url, "cache_object://%[^/]/%[^@?]%n@%[^?]?%s", host, request, &pos, password, params); | |
5366b99b AJ |
173 | if (t < 3) { |
174 | t = sscanf(url, "cache_object://%[^/]/%[^?]%n?%s", host, request, &pos, params); | |
175 | } | |
e37bd29b AJ |
176 | if (t < 1) { |
177 | t = sscanf(url, "http://%[^/]/squid-internal-mgr/%[^?]%n?%s", host, request, &pos, params); | |
178 | } | |
179 | if (t < 1) { | |
180 | t = sscanf(url, "https://%[^/]/squid-internal-mgr/%[^?]%n?%s", host, request, &pos, params); | |
181 | } | |
f9c6f861 AJ |
182 | if (t < 2) { |
183 | if (strncmp("cache_object://",url,15)==0) | |
184 | xstrncpy(request, "menu", MAX_URL); | |
185 | else | |
186 | xstrncpy(request, "index", MAX_URL); | |
187 | } | |
8822ebee | 188 | |
1191b93b | 189 | #if _SQUID_OS2_ |
8822ebee | 190 | if (t == 2 && request[0] == '\0') { |
62e76326 | 191 | /* |
192 | * emx's sscanf insists of returning 2 because it sets request | |
193 | * to null | |
194 | */ | |
f9c6f861 AJ |
195 | if (strncmp("cache_object://",url,15)==0) |
196 | xstrncpy(request, "menu", MAX_URL); | |
197 | else | |
198 | xstrncpy(request, "index", MAX_URL); | |
8822ebee | 199 | } |
cd377065 | 200 | #endif |
62e76326 | 201 | |
5366b99b AJ |
202 | debugs(16, 3, HERE << "MGR request: t=" << t << ", host='" << host << "', request='" << request << "', pos=" << pos << |
203 | ", password='" << password << "', params='" << params << "'"); | |
204 | ||
8822ebee AR |
205 | Mgr::ActionProfile::Pointer profile = findAction(request); |
206 | if (!profile) { | |
3e1da049 | 207 | debugs(16, DBG_IMPORTANT, "CacheManager::ParseUrl: action '" << request << "' not found"); |
62e76326 | 208 | return NULL; |
22f3fd98 | 209 | } |
62e76326 | 210 | |
8822ebee AR |
211 | const char *prot = ActionProtection(profile); |
212 | if (!strcmp(prot, "disabled") || !strcmp(prot, "hidden")) { | |
213 | debugs(16, DBG_IMPORTANT, "CacheManager::ParseUrl: action '" << request << "' is " << prot); | |
214 | return NULL; | |
215 | } | |
62e76326 | 216 | |
8822ebee | 217 | Mgr::Command::Pointer cmd = new Mgr::Command; |
b8151fa1 CT |
218 | if (!Mgr::QueryParams::Parse(params, cmd->params.queryParams)) |
219 | return NULL; | |
8822ebee AR |
220 | cmd->profile = profile; |
221 | cmd->params.httpUri = url; | |
222 | cmd->params.userName = String(); | |
b8151fa1 | 223 | cmd->params.password = password; |
8822ebee AR |
224 | cmd->params.actionName = request; |
225 | return cmd; | |
22f3fd98 | 226 | } |
227 | ||
63be0a78 | 228 | /// \ingroup CacheManagerInternal |
832c08ab FC |
229 | /* |
230 | \ingroup CacheManagerInternal | |
231 | * Decodes the headers needed to perform user authentication and fills | |
232 | * the details into the cachemgrStateData argument | |
233 | */ | |
c83f0bd5 | 234 | void |
8822ebee | 235 | CacheManager::ParseHeaders(const HttpRequest * request, Mgr::ActionParams ¶ms) |
63259c34 | 236 | { |
8822ebee AR |
237 | assert(request); |
238 | ||
239 | params.httpMethod = request->method.id(); | |
240 | params.httpFlags = request->flags; | |
241 | ||
9da6b594 AJ |
242 | #if HAVE_AUTH_MODULE_BASIC |
243 | // TODO: use the authentication system decode to retrieve these details properly. | |
244 | ||
245 | /* base 64 _decoded_ user:passwd pair */ | |
789217a2 | 246 | const char *basic_cookie = request->header.getAuth(Http::HdrType::AUTHORIZATION, "Basic"); |
62e76326 | 247 | |
99edd1c3 | 248 | if (!basic_cookie) |
62e76326 | 249 | return; |
250 | ||
9da6b594 | 251 | const char *passwd_del; |
63259c34 | 252 | if (!(passwd_del = strchr(basic_cookie, ':'))) { |
3e1da049 | 253 | debugs(16, DBG_IMPORTANT, "CacheManager::ParseHeaders: unknown basic_cookie format '" << basic_cookie << "'"); |
62e76326 | 254 | return; |
63259c34 | 255 | } |
62e76326 | 256 | |
63259c34 | 257 | /* found user:password pair, reset old values */ |
8822ebee AR |
258 | params.userName.limitInit(basic_cookie, passwd_del - basic_cookie); |
259 | params.password = passwd_del + 1; | |
62e76326 | 260 | |
9da6b594 | 261 | /* warning: this prints decoded password which maybe not be what you want to do @?@ @?@ */ |
8822ebee | 262 | debugs(16, 9, "CacheManager::ParseHeaders: got user: '" << |
d9fc6862 | 263 | params.userName << "' passwd: '" << params.password << "'"); |
9da6b594 | 264 | #endif |
63259c34 | 265 | } |
266 | ||
63be0a78 | 267 | /** |
268 | \ingroup CacheManagerInternal | |
269 | * | |
f53969cc SM |
270 | \retval 0 if mgr->password is good or "none" |
271 | \retval 1 if mgr->password is "disable" | |
272 | \retval !0 if mgr->password does not match configured password | |
22f3fd98 | 273 | */ |
c83f0bd5 | 274 | int |
8822ebee | 275 | CacheManager::CheckPassword(const Mgr::Command &cmd) |
22f3fd98 | 276 | { |
8822ebee AR |
277 | assert(cmd.profile != NULL); |
278 | const char *action = cmd.profile->name; | |
279 | char *pwd = PasswdGet(Config.passwd_list, action); | |
03c4599f | 280 | |
8822ebee | 281 | debugs(16, 4, "CacheManager::CheckPassword for action " << action); |
62e76326 | 282 | |
22f3fd98 | 283 | if (pwd == NULL) |
8822ebee | 284 | return cmd.profile->isPwReq; |
62e76326 | 285 | |
22f3fd98 | 286 | if (strcmp(pwd, "disable") == 0) |
62e76326 | 287 | return 1; |
288 | ||
22f3fd98 | 289 | if (strcmp(pwd, "none") == 0) |
62e76326 | 290 | return 0; |
291 | ||
8822ebee | 292 | if (!cmd.params.password.size()) |
62e76326 | 293 | return 1; |
294 | ||
8822ebee | 295 | return cmd.params.password != pwd; |
22f3fd98 | 296 | } |
297 | ||
832c08ab FC |
298 | /** |
299 | \ingroup CacheManagerAPI | |
300 | * Main entry point in the Cache Manager's activity. Gets called as part | |
301 | * of the forward chain if the right URL is detected there. Initiates | |
302 | * all needed internal work and renders the response. | |
303 | */ | |
22f3fd98 | 304 | void |
5c336a3b | 305 | CacheManager::Start(const Comm::ConnectionPointer &client, HttpRequest * request, StoreEntry * entry) |
22f3fd98 | 306 | { |
832c08ab | 307 | debugs(16, 3, "CacheManager::Start: '" << entry->url() << "'" ); |
62e76326 | 308 | |
8822ebee AR |
309 | Mgr::Command::Pointer cmd = ParseUrl(entry->url()); |
310 | if (!cmd) { | |
955394ce | 311 | ErrorState *err = new ErrorState(ERR_INVALID_URL, Http::scNotFound, request); |
3900307b | 312 | err->url = xstrdup(entry->url()); |
62e76326 | 313 | errorAppendEntry(entry, err); |
314 | entry->expires = squid_curtime; | |
315 | return; | |
22f3fd98 | 316 | } |
62e76326 | 317 | |
8822ebee | 318 | const char *actionName = cmd->profile->name; |
34266cde | 319 | |
22f3fd98 | 320 | entry->expires = squid_curtime; |
34266cde | 321 | |
a750e510 | 322 | debugs(16, 5, "CacheManager: " << client << " requesting '" << actionName << "'"); |
34266cde | 323 | |
63259c34 | 324 | /* get additional info from request headers */ |
8822ebee AR |
325 | ParseHeaders(request, cmd->params); |
326 | ||
327 | const char *userName = cmd->params.userName.size() ? | |
d9fc6862 | 328 | cmd->params.userName.termedBuf() : "unknown"; |
34266cde | 329 | |
22f3fd98 | 330 | /* Check password */ |
62e76326 | 331 | |
8822ebee | 332 | if (CheckPassword(*cmd) != 0) { |
62e76326 | 333 | /* build error message */ |
955394ce | 334 | ErrorState errState(ERR_CACHE_MGR_ACCESS_DENIED, Http::scUnauthorized, request); |
62e76326 | 335 | /* warn if user specified incorrect password */ |
336 | ||
8822ebee | 337 | if (cmd->params.password.size()) { |
26ac0430 | 338 | debugs(16, DBG_IMPORTANT, "CacheManager: " << |
8822ebee | 339 | userName << "@" << |
5c336a3b | 340 | client << ": incorrect password for '" << |
8822ebee AR |
341 | actionName << "'" ); |
342 | } else { | |
26ac0430 | 343 | debugs(16, DBG_IMPORTANT, "CacheManager: " << |
8822ebee | 344 | userName << "@" << |
5c336a3b | 345 | client << ": password needed for '" << |
8822ebee AR |
346 | actionName << "'" ); |
347 | } | |
62e76326 | 348 | |
913524f0 | 349 | HttpReply *rep = errState.BuildHttpReply(); |
62e76326 | 350 | |
9da6b594 | 351 | #if HAVE_AUTH_MODULE_BASIC |
62e76326 | 352 | /* |
8822ebee AR |
353 | * add Authenticate header using action name as a realm because |
354 | * password depends on the action | |
62e76326 | 355 | */ |
8822ebee | 356 | rep->header.putAuth("Basic", actionName); |
9da6b594 | 357 | #endif |
3865965d | 358 | // Allow cachemgr and other XHR scripts access to our version string |
789217a2 FC |
359 | if (request->header.has(Http::HdrType::ORIGIN)) { |
360 | rep->header.putExt("Access-Control-Allow-Origin",request->header.getStr(Http::HdrType::ORIGIN)); | |
3865965d AJ |
361 | #if HAVE_AUTH_MODULE_BASIC |
362 | rep->header.putExt("Access-Control-Allow-Credentials","true"); | |
363 | #endif | |
364 | rep->header.putExt("Access-Control-Expose-Headers","Server"); | |
365 | } | |
62e76326 | 366 | |
367 | /* store the reply */ | |
db237875 | 368 | entry->replaceHttpReply(rep); |
62e76326 | 369 | |
370 | entry->expires = squid_curtime; | |
371 | ||
372 | entry->complete(); | |
373 | ||
62e76326 | 374 | return; |
22f3fd98 | 375 | } |
62e76326 | 376 | |
789217a2 FC |
377 | if (request->header.has(Http::HdrType::ORIGIN)) { |
378 | cmd->params.httpOrigin = request->header.getStr(Http::HdrType::ORIGIN); | |
3865965d AJ |
379 | } |
380 | ||
0be039f4 | 381 | debugs(16, 2, "CacheManager: " << |
8822ebee | 382 | userName << "@" << |
5c336a3b | 383 | client << " requesting '" << |
8822ebee | 384 | actionName << "'" ); |
62e76326 | 385 | |
b073fc4b AJ |
386 | // special case: /squid-internal-mgr/ index page |
387 | if (!strcmp(cmd->profile->name, "index")) { | |
955394ce | 388 | ErrorState err(MGR_INDEX, Http::scOkay, request); |
b073fc4b AJ |
389 | err.url = xstrdup(entry->url()); |
390 | HttpReply *rep = err.BuildHttpReply(); | |
391 | if (strncmp(rep->body.content(),"Internal Error:", 15) == 0) | |
9b769c67 | 392 | rep->sline.set(Http::ProtocolVersion(1,1), Http::scNotFound); |
b073fc4b | 393 | // Allow cachemgr and other XHR scripts access to our version string |
789217a2 FC |
394 | if (request->header.has(Http::HdrType::ORIGIN)) { |
395 | rep->header.putExt("Access-Control-Allow-Origin",request->header.getStr(Http::HdrType::ORIGIN)); | |
b073fc4b AJ |
396 | #if HAVE_AUTH_MODULE_BASIC |
397 | rep->header.putExt("Access-Control-Allow-Credentials","true"); | |
398 | #endif | |
399 | rep->header.putExt("Access-Control-Expose-Headers","Server"); | |
400 | } | |
401 | entry->replaceHttpReply(rep); | |
402 | entry->complete(); | |
403 | return; | |
404 | } | |
405 | ||
8822ebee | 406 | if (UsingSmp() && IamWorkerProcess()) { |
1b76e6c1 | 407 | // is client the right connection to pass here? |
25b481e6 | 408 | AsyncJob::Start(new Mgr::Forwarder(client, cmd->params, request, entry)); |
8822ebee | 409 | return; |
cb69b4c7 | 410 | } |
62e76326 | 411 | |
8822ebee AR |
412 | Mgr::Action::Pointer action = cmd->profile->creator->create(cmd); |
413 | Must(action != NULL); | |
414 | action->run(entry, true); | |
22f3fd98 | 415 | } |
416 | ||
832c08ab FC |
417 | /* |
418 | \ingroup CacheManagerInternal | |
419 | * Renders the protection level text for an action. | |
420 | * Also doubles as a check for the protection level. | |
832c08ab | 421 | */ |
c83f0bd5 | 422 | const char * |
8822ebee | 423 | CacheManager::ActionProtection(const Mgr::ActionProfile::Pointer &profile) |
7395afb8 | 424 | { |
8822ebee AR |
425 | assert(profile != NULL); |
426 | const char *pwd = PasswdGet(Config.passwd_list, profile->name); | |
62e76326 | 427 | |
7395afb8 | 428 | if (!pwd) |
8822ebee | 429 | return profile->isPwReq ? "hidden" : "public"; |
62e76326 | 430 | |
7395afb8 | 431 | if (!strcmp(pwd, "disable")) |
62e76326 | 432 | return "disabled"; |
433 | ||
7395afb8 | 434 | if (strcmp(pwd, "none") == 0) |
62e76326 | 435 | return "public"; |
436 | ||
7395afb8 | 437 | return "protected"; |
438 | } | |
439 | ||
832c08ab | 440 | /* |
ee82937c | 441 | * \ingroup CacheManagerInternal |
832c08ab FC |
442 | * gets from the global Config the password the user would need to supply |
443 | * for the action she queried | |
444 | */ | |
c83f0bd5 | 445 | char * |
613924ee | 446 | CacheManager::PasswdGet(Mgr::ActionPasswordList * a, const char *action) |
22f3fd98 | 447 | { |
a5f27c62 AJ |
448 | while (a) { |
449 | for (auto &w : a->actions) { | |
450 | if (w.cmp(action) == 0) | |
62e76326 | 451 | return a->passwd; |
452 | ||
a5f27c62 AJ |
453 | static const SBuf allAction("all"); |
454 | if (w == allAction) | |
62e76326 | 455 | return a->passwd; |
456 | } | |
457 | ||
458 | a = a->next; | |
22f3fd98 | 459 | } |
62e76326 | 460 | |
22f3fd98 | 461 | return NULL; |
462 | } | |
c83f0bd5 | 463 | |
c83f0bd5 | 464 | CacheManager* |
26ac0430 AJ |
465 | CacheManager::GetInstance() |
466 | { | |
f0ffd7c3 | 467 | static CacheManager *instance = nullptr; |
7658a296 AJ |
468 | if (!instance) { |
469 | debugs(16, 6, "starting cachemanager up"); | |
26ac0430 | 470 | instance = new CacheManager; |
8822ebee | 471 | Mgr::RegisterBasics(); |
26ac0430 AJ |
472 | } |
473 | return instance; | |
c83f0bd5 | 474 | } |
f53969cc | 475 |