]>
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 29 Authenticator */ | |
10 | ||
11 | /* The functions in this file handle authentication. | |
12 | * They DO NOT perform access control or auditing. | |
13 | * See acl.c for access control and client_side.c for auditing */ | |
14 | ||
15 | #include "squid.h" | |
16 | #include "auth/basic/Config.h" | |
17 | #include "auth/basic/Scheme.h" | |
18 | #include "auth/basic/User.h" | |
19 | #include "auth/basic/UserRequest.h" | |
20 | #include "auth/CredentialsCache.h" | |
21 | #include "auth/Gadgets.h" | |
22 | #include "auth/State.h" | |
23 | #include "auth/toUtf.h" | |
24 | #include "base64.h" | |
25 | #include "cache_cf.h" | |
26 | #include "helper.h" | |
27 | #include "HttpHeaderTools.h" | |
28 | #include "HttpReply.h" | |
29 | #include "mgr/Registration.h" | |
30 | #include "rfc1738.h" | |
31 | #include "sbuf/SBuf.h" | |
32 | #include "Store.h" | |
33 | #include "util.h" | |
34 | #include "wordlist.h" | |
35 | ||
36 | /* Basic Scheme */ | |
37 | static AUTHSSTATS authenticateBasicStats; | |
38 | ||
39 | Helper::ClientPointer basicauthenticators; | |
40 | ||
41 | static int authbasic_initialised = 0; | |
42 | ||
43 | /* | |
44 | * | |
45 | * Public Functions | |
46 | * | |
47 | */ | |
48 | ||
49 | /* internal functions */ | |
50 | ||
51 | bool | |
52 | Auth::Basic::Config::active() const | |
53 | { | |
54 | return authbasic_initialised == 1; | |
55 | } | |
56 | ||
57 | bool | |
58 | Auth::Basic::Config::configured() const | |
59 | { | |
60 | return (SchemeConfig::configured() && !realm.isEmpty()); | |
61 | } | |
62 | ||
63 | const char * | |
64 | Auth::Basic::Config::type() const | |
65 | { | |
66 | return Auth::Basic::Scheme::GetInstance()->type(); | |
67 | } | |
68 | ||
69 | void | |
70 | Auth::Basic::Config::fixHeader(Auth::UserRequest::Pointer, HttpReply *rep, Http::HdrType hdrType, HttpRequest *) | |
71 | { | |
72 | if (authenticateProgram) { | |
73 | if (utf8) { | |
74 | debugs(29, 9, "Sending type:" << hdrType << " header: 'Basic realm=\"" << realm << "\", charset=\"UTF-8\"'"); | |
75 | httpHeaderPutStrf(&rep->header, hdrType, "Basic realm=\"" SQUIDSBUFPH "\", charset=\"UTF-8\"", SQUIDSBUFPRINT(realm)); | |
76 | } else { | |
77 | debugs(29, 9, "Sending type:" << hdrType << " header: 'Basic realm=\"" << realm << "\"'"); | |
78 | httpHeaderPutStrf(&rep->header, hdrType, "Basic realm=\"" SQUIDSBUFPH "\"", SQUIDSBUFPRINT(realm)); | |
79 | } | |
80 | } | |
81 | } | |
82 | ||
83 | void | |
84 | Auth::Basic::Config::rotateHelpers() | |
85 | { | |
86 | /* schedule closure of existing helpers */ | |
87 | if (basicauthenticators) { | |
88 | helperShutdown(basicauthenticators); | |
89 | } | |
90 | ||
91 | /* NP: dynamic helper restart will ensure they start up again as needed. */ | |
92 | } | |
93 | ||
94 | /** shutdown the auth helpers and free any allocated configuration details */ | |
95 | void | |
96 | Auth::Basic::Config::done() | |
97 | { | |
98 | Auth::SchemeConfig::done(); | |
99 | ||
100 | authbasic_initialised = 0; | |
101 | ||
102 | if (basicauthenticators) { | |
103 | helperShutdown(basicauthenticators); | |
104 | } | |
105 | ||
106 | basicauthenticators = nullptr; | |
107 | ||
108 | if (authenticateProgram) | |
109 | wordlistDestroy(&authenticateProgram); | |
110 | } | |
111 | ||
112 | bool | |
113 | Auth::Basic::Config::dump(StoreEntry * entry, const char *name, Auth::SchemeConfig * scheme) const | |
114 | { | |
115 | if (!Auth::SchemeConfig::dump(entry, name, scheme)) | |
116 | return false; // not configured | |
117 | ||
118 | storeAppendPrintf(entry, "%s basic credentialsttl %d seconds\n", name, (int) credentialsTTL); | |
119 | storeAppendPrintf(entry, "%s basic casesensitive %s\n", name, casesensitive ? "on" : "off"); | |
120 | return true; | |
121 | } | |
122 | ||
123 | Auth::Basic::Config::Config() : | |
124 | credentialsTTL( 2*60*60 ), | |
125 | casesensitive(0) | |
126 | { | |
127 | static const SBuf defaultRealm("Squid proxy-caching web server"); | |
128 | realm = defaultRealm; | |
129 | } | |
130 | ||
131 | void | |
132 | Auth::Basic::Config::parse(Auth::SchemeConfig * scheme, size_t n_configured, char *param_str) | |
133 | { | |
134 | if (strcmp(param_str, "credentialsttl") == 0) { | |
135 | parse_time_t(&credentialsTTL); | |
136 | } else if (strcmp(param_str, "casesensitive") == 0) { | |
137 | parse_onoff(&casesensitive); | |
138 | } else | |
139 | Auth::SchemeConfig::parse(scheme, n_configured, param_str); | |
140 | } | |
141 | ||
142 | static void | |
143 | authenticateBasicStats(StoreEntry * sentry) | |
144 | { | |
145 | if (basicauthenticators) | |
146 | basicauthenticators->packStatsInto(sentry, "Basic Authenticator Statistics"); | |
147 | } | |
148 | ||
149 | char * | |
150 | Auth::Basic::Config::decodeCleartext(const char *httpAuthHeader, const HttpRequest *request) | |
151 | { | |
152 | const char *proxy_auth = httpAuthHeader; | |
153 | ||
154 | /* trim BASIC from string */ | |
155 | while (xisgraph(*proxy_auth)) | |
156 | ++proxy_auth; | |
157 | ||
158 | /* Trim leading whitespace before decoding */ | |
159 | while (xisspace(*proxy_auth)) | |
160 | ++proxy_auth; | |
161 | ||
162 | /* Trim trailing \n before decoding */ | |
163 | // XXX: really? is the \n actually still there? does the header parse not drop it? | |
164 | char *eek = xstrdup(proxy_auth); | |
165 | strtok(eek, "\n"); | |
166 | ||
167 | const size_t srcLen = strlen(eek); | |
168 | char *cleartext = static_cast<char*>(xmalloc(BASE64_DECODE_LENGTH(srcLen)+1)); | |
169 | ||
170 | struct base64_decode_ctx ctx; | |
171 | base64_decode_init(&ctx); | |
172 | ||
173 | size_t dstLen = 0; | |
174 | if (base64_decode_update(&ctx, &dstLen, reinterpret_cast<uint8_t*>(cleartext), srcLen, eek) && base64_decode_final(&ctx)) { | |
175 | cleartext[dstLen] = '\0'; | |
176 | ||
177 | if (utf8 && !isValidUtf8String(cleartext, cleartext + dstLen)) { | |
178 | auto str = isCP1251EncodingAllowed(request) ? | |
179 | Cp1251ToUtf8(cleartext) : Latin1ToUtf8(cleartext); | |
180 | safe_free(cleartext); | |
181 | cleartext = xstrdup(str.c_str()); | |
182 | } | |
183 | ||
184 | /* | |
185 | * Don't allow NL or CR in the credentials. | |
186 | * Oezguer Kesim <oec@codeblau.de> | |
187 | */ | |
188 | debugs(29, 9, "'" << cleartext << "'"); | |
189 | ||
190 | if (strcspn(cleartext, "\r\n") != strlen(cleartext)) { | |
191 | debugs(29, DBG_IMPORTANT, "WARNING: Bad characters in authorization header '" << httpAuthHeader << "'"); | |
192 | safe_free(cleartext); | |
193 | } | |
194 | } else { | |
195 | debugs(29, 2, "WARNING: Invalid Base64 character in authorization header '" << httpAuthHeader << "'"); | |
196 | safe_free(cleartext); | |
197 | } | |
198 | ||
199 | safe_free(eek); | |
200 | return cleartext; | |
201 | } | |
202 | ||
203 | /** | |
204 | * Decode a Basic [Proxy-]Auth string, linking the passed | |
205 | * auth_user_request structure to any existing user structure or creating one | |
206 | * if needed. Note that just returning will be treated as | |
207 | * "cannot decode credentials". Use the message field to return a | |
208 | * descriptive message to the user. | |
209 | */ | |
210 | Auth::UserRequest::Pointer | |
211 | Auth::Basic::Config::decode(char const *proxy_auth, const HttpRequest *request, const char *aRequestRealm) | |
212 | { | |
213 | Auth::UserRequest::Pointer auth_user_request = dynamic_cast<Auth::UserRequest*>(new Auth::Basic::UserRequest); | |
214 | /* decode the username */ | |
215 | ||
216 | // retrieve the cleartext (in a dynamically allocated char*) | |
217 | const auto cleartext = decodeCleartext(proxy_auth, request); | |
218 | ||
219 | // empty header? no auth details produced... | |
220 | if (!cleartext) | |
221 | return auth_user_request; | |
222 | ||
223 | Auth::User::Pointer lb; | |
224 | /* permitted because local_basic is purely local function scope. */ | |
225 | Auth::Basic::User *local_basic = nullptr; | |
226 | ||
227 | char *separator = strchr(cleartext, ':'); | |
228 | ||
229 | lb = local_basic = new Auth::Basic::User(this, aRequestRealm); | |
230 | ||
231 | if (separator) { | |
232 | /* terminate the username */ | |
233 | *separator = '\0'; | |
234 | local_basic->passwd = xstrdup(separator+1); | |
235 | } | |
236 | ||
237 | if (!casesensitive) | |
238 | Tolower(cleartext); | |
239 | local_basic->username(cleartext); | |
240 | ||
241 | if (local_basic->passwd == nullptr) { | |
242 | debugs(29, 4, "no password in proxy authorization header '" << proxy_auth << "'"); | |
243 | auth_user_request->setDenyMessage("no password was present in the HTTP [proxy-]authorization header. This is most likely a browser bug"); | |
244 | } else { | |
245 | if (local_basic->passwd[0] == '\0') { | |
246 | debugs(29, 4, "Disallowing empty password. User is '" << local_basic->username() << "'"); | |
247 | safe_free(local_basic->passwd); | |
248 | auth_user_request->setDenyMessage("Request denied because you provided an empty password. Users MUST have a password."); | |
249 | } | |
250 | } | |
251 | ||
252 | xfree(cleartext); | |
253 | ||
254 | if (!local_basic->valid()) { | |
255 | lb->auth_type = Auth::AUTH_BROKEN; | |
256 | auth_user_request->user(lb); | |
257 | return auth_user_request; | |
258 | } | |
259 | ||
260 | /* now lookup and see if we have a matching auth_user structure in memory. */ | |
261 | Auth::User::Pointer auth_user; | |
262 | ||
263 | if (!(auth_user = Auth::Basic::User::Cache()->lookup(lb->userKey()))) { | |
264 | /* the user doesn't exist in the username cache yet */ | |
265 | /* save the credentials */ | |
266 | debugs(29, 9, "Creating new user '" << lb->username() << "'"); | |
267 | /* set the auth_user type */ | |
268 | lb->auth_type = Auth::AUTH_BASIC; | |
269 | /* current time for timeouts */ | |
270 | lb->expiretime = current_time.tv_sec; | |
271 | ||
272 | /* this basic_user struct is the 'lucky one' to get added to the username cache */ | |
273 | /* the requests after this link to the basic_user */ | |
274 | /* store user in hash */ | |
275 | lb->addToNameCache(); | |
276 | ||
277 | auth_user = lb; | |
278 | assert(auth_user != nullptr); | |
279 | } else { | |
280 | /* replace the current cached password with the new one */ | |
281 | Auth::Basic::User *basic_auth = dynamic_cast<Auth::Basic::User *>(auth_user.getRaw()); | |
282 | assert(basic_auth); | |
283 | basic_auth->updateCached(local_basic); | |
284 | auth_user = basic_auth; | |
285 | } | |
286 | ||
287 | /* link the request to the in-cache user */ | |
288 | auth_user_request->user(auth_user); | |
289 | return auth_user_request; | |
290 | } | |
291 | ||
292 | /** Initialize helpers and the like for this auth scheme. Called AFTER parsing the | |
293 | * config file */ | |
294 | void | |
295 | Auth::Basic::Config::init(Auth::SchemeConfig *) | |
296 | { | |
297 | if (authenticateProgram) { | |
298 | authbasic_initialised = 1; | |
299 | ||
300 | if (basicauthenticators == nullptr) | |
301 | basicauthenticators = Helper::Client::Make("basicauthenticator"); | |
302 | ||
303 | basicauthenticators->cmdline = authenticateProgram; | |
304 | ||
305 | basicauthenticators->childs.updateLimits(authenticateChildren); | |
306 | ||
307 | basicauthenticators->ipc_type = IPC_STREAM; | |
308 | ||
309 | basicauthenticators->openSessions(); | |
310 | } | |
311 | } | |
312 | ||
313 | void | |
314 | Auth::Basic::Config::registerWithCacheManager(void) | |
315 | { | |
316 | Mgr::RegisterAction("basicauthenticator", | |
317 | "Basic User Authenticator Stats", | |
318 | authenticateBasicStats, 0, 1); | |
319 | } | |
320 |