]> git.ipfire.org Git - thirdparty/squid.git/blob - src/helper/Reply.cc
Fix and improve annotation reporting (#1516)
[thirdparty/squid.git] / src / helper / Reply.cc
1 /*
2 * Copyright (C) 1996-2023 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 84 Helper process maintenance */
10
11 #include "squid.h"
12 #include "ConfigParser.h"
13 #include "debug/Messages.h"
14 #include "debug/Stream.h"
15 #include "helper.h"
16 #include "helper/Reply.h"
17 #include "rfc1738.h"
18 #include "SquidString.h"
19
20 Helper::Reply::Reply() :
21 result(Helper::Unknown)
22 {
23 }
24
25 bool
26 Helper::Reply::accumulate(const char *buf, size_t len)
27 {
28 if (other_.isNull())
29 other_.init(4*1024, 1*1024*1024);
30
31 if (other_.potentialSpaceSize() < static_cast<mb_size_t>(len))
32 return false; // no space left
33
34 other_.append(buf, len);
35 return true;
36 }
37
38 void
39 Helper::Reply::finalize()
40 {
41 debugs(84, 3, "Parsing helper buffer");
42 // check we have something to parse
43 if (!other_.hasContent()) {
44 // empty line response was the old URL-rewriter interface ERR response.
45 result = Helper::Error;
46 // for now ensure that legacy handlers are not presented with NULL strings.
47 debugs(84, 3, "Zero length reply");
48 return;
49 }
50
51 char *p = other_.content();
52 size_t len = other_.contentSize();
53 bool sawNA = false;
54
55 // optimization: do not consider parsing result code if the response is short.
56 // URL-rewriter may return relative URLs or empty response for a large portion
57 // of its replies.
58 if (len >= 2) {
59 debugs(84, 3, "Buff length is larger than 2");
60 // some helper formats (digest auth, URL-rewriter) just send a data string
61 // we must also check for the ' ' character after the response token (if anything)
62 if (!strncmp(p,"OK",2) && (len == 2 || p[2] == ' ')) {
63 debugs(84, 3, "helper Result = OK");
64 result = Helper::Okay;
65 p+=2;
66 } else if (!strncmp(p,"ERR",3) && (len == 3 || p[3] == ' ')) {
67 debugs(84, 3, "helper Result = ERR");
68 result = Helper::Error;
69 p+=3;
70 } else if (!strncmp(p,"BH",2) && (len == 2 || p[2] == ' ')) {
71 debugs(84, 3, "helper Result = BH");
72 result = Helper::BrokenHelper;
73 p+=2;
74 } else if (!strncmp(p,"TT ",3)) {
75 // NTLM challenge token
76 result = Helper::TT;
77 p+=3;
78 // followed by an auth token
79 char *w1 = strwordtok(nullptr, &p);
80 if (w1 != nullptr) {
81 const char *authToken = w1;
82 notes.add("token",authToken);
83 } else {
84 // token field is mandatory on this response code
85 result = Helper::BrokenHelper;
86 notes.add("message","Missing 'token' data");
87 }
88
89 } else if (!strncmp(p,"AF ",3)) {
90 // NTLM/Negotiate OK response
91 result = Helper::Okay;
92 p+=3;
93 // followed by:
94 // an optional auth token and user field
95 // or, an optional username field
96 char *w1 = strwordtok(nullptr, &p);
97 char *w2 = strwordtok(nullptr, &p);
98 if (w2 != nullptr) {
99 // Negotiate "token user"
100 const char *authToken = w1;
101 notes.add("token",authToken);
102
103 const char *user = w2;
104 notes.add("user",user);
105
106 } else if (w1 != nullptr) {
107 // NTLM "user"
108 const char *user = w1;
109 notes.add("user",user);
110 }
111 } else if (!strncmp(p,"NA ",3)) {
112 // NTLM fail-closed ERR response
113 result = Helper::Error;
114 p+=3;
115 sawNA=true;
116 }
117
118 for (; xisspace(*p); ++p); // skip whitespace
119 }
120
121 other_.consume(p - other_.content());
122 other_.consumeWhitespacePrefix();
123
124 // Hack for backward-compatibility: Do not parse for kv-pairs on NA response
125 if (!sawNA)
126 parseResponseKeys();
127
128 // Hack for backward-compatibility: BH and NA used to be a text message...
129 if (other_.hasContent() && (sawNA || result == Helper::BrokenHelper)) {
130 notes.add("message", other_.content());
131 other_.clean();
132 }
133 }
134
135 /// restrict key names to alphanumeric, hyphen, underscore characters
136 static bool
137 isKeyNameChar(char c)
138 {
139 if (c >= 'a' && c <= 'z')
140 return true;
141
142 if (c >= 'A' && c <= 'Z')
143 return true;
144
145 if (c >= '0' && c <= '9')
146 return true;
147
148 if (c == '-' || c == '_')
149 return true;
150
151 // prevent other characters matching the key=value
152 return false;
153 }
154
155 /// warns admin about problematic key=value pairs
156 void
157 Helper::Reply::CheckReceivedKey(const SBuf &key, const SBuf &value)
158 {
159 // Squid recognizes these keys (by name) in some helper responses
160 static const std::vector<SBuf> recognized = {
161 SBuf("clt_conn_tag"),
162 SBuf("group"),
163 SBuf("ha1"),
164 SBuf("log"),
165 SBuf("message"),
166 SBuf("nonce"),
167 SBuf("password"),
168 SBuf("rewrite-url"),
169 SBuf("status"),
170 SBuf("store-id"),
171 SBuf("tag"),
172 SBuf("token"),
173 SBuf("url"),
174 SBuf("user")
175 };
176
177 // TODO: Merge with Notes::ReservedKeys(). That list has an entry that Squid
178 // sources do _not_ recognize today ("ttl"), and it is missing some
179 // recognized entries ("clt_conn_tag", "nonce", store-id", and "token").
180
181 if (key.isEmpty()) {
182 debugs(84, DBG_IMPORTANT, "WARNING: Deprecated from-helper annotation without a name: " <<
183 key << '=' << value <<
184 Debug::Extra << "advice: Name or remove this annotation");
185 // TODO: Skip/ignore these annotations.
186 return;
187 }
188
189 // We do not check custom keys for repetitions because Squid supports them:
190 // The "note" ACL checks all of them and %note prints all of them.
191 if (*key.rbegin() == '_')
192 return; // a custom key
193
194 // To simplify, we allow all recognized keys, even though some of them are
195 // only expected from certain helpers or even only in certain reply types.
196 // To simplify and optimize, we do not check recognized keys for repetitions
197 // because _some_ of them (e.g., "message") do support repetitions.
198 if (std::find(recognized.begin(), recognized.end(), key) != recognized.end())
199 return; // a Squid-recognized key
200
201 debugs(84, Important(69), "WARNING: Unsupported or unexpected from-helper annotation with a name reserved for Squid use: " <<
202 key << '=' << value <<
203 Debug::Extra << "advice: If this is a custom annotation, rename it to add a trailing underscore: " <<
204 key << '_');
205 }
206
207 void
208 Helper::Reply::parseResponseKeys()
209 {
210 // parse a "key=value" pair off the 'other()' buffer.
211 while (other_.hasContent()) {
212 char *p = other_.content();
213 const char *key = p;
214 while (*p && isKeyNameChar(*p)) ++p;
215 if (*p != '=')
216 return; // done. Not a key.
217
218 // whitespace between key and value is prohibited.
219 // workaround strwordtok() which skips whitespace prefix.
220 if (xisspace(*(p+1)))
221 return; // done. Not a key.
222
223 *p = '\0';
224 ++p;
225
226 // the value may be a quoted string or a token
227 const bool urlDecode = (*p != '"'); // check before moving p.
228 char *v = strwordtok(nullptr, &p);
229 if (v != nullptr && urlDecode && (p-v) > 2) // 1-octet %-escaped requires 3 bytes
230 rfc1738_unescape(v);
231
232 // TODO: Convert the above code to use Tokenizer and SBuf
233 const SBuf parsedKey(key);
234 const SBuf parsedValue(v); // allow empty values (!v or !*v)
235 CheckReceivedKey(parsedKey, parsedValue);
236 notes.add(parsedKey, parsedValue);
237
238 other_.consume(p - other_.content());
239 other_.consumeWhitespacePrefix();
240 }
241 }
242
243 const MemBuf &
244 Helper::Reply::emptyBuf() const
245 {
246 static MemBuf empty;
247 if (empty.isNull())
248 empty.init(1, 1);
249 return empty;
250 }
251
252 std::ostream &
253 Helper::operator <<(std::ostream &os, const Reply &r)
254 {
255 os << "{result=";
256 switch (r.result) {
257 case Okay:
258 os << "OK";
259 break;
260 case Error:
261 os << "ERR";
262 break;
263 case BrokenHelper:
264 os << "BH";
265 break;
266 case TT:
267 os << "TT";
268 break;
269 case TimedOut:
270 os << "Timeout";
271 break;
272 case Unknown:
273 os << "Unknown";
274 break;
275 }
276
277 // dump the helper key=pair "notes" list
278 if (!r.notes.empty()) {
279 os << ", notes={";
280 // This simple format matches what most helpers use and is sufficient
281 // for debugging nearly any helper response, but the result differs from
282 // raw helper responses when the helper quotes values or escapes special
283 // characters. See also: Helper::Reply::parseResponseKeys().
284 r.notes.print(os, "=", " ");
285 os << "}";
286 }
287
288 MemBuf const &o = r.other();
289 if (o.hasContent())
290 os << ", other: \"" << o.content() << '\"';
291
292 os << '}';
293
294 return os;
295 }
296