2 * Copyright (C) 1996-2025 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 52 URN Parsing */
12 #include "AccessLogEntry.h"
13 #include "acl/FilledChecklist.h"
14 #include "base/TextException.h"
16 #include "errorpage.h"
19 #include "HttpReply.h"
20 #include "HttpRequest.h"
21 #include "icmp/net_db.h"
23 #include "mime_header.h"
24 #include "RequestFlags.h"
26 #include "StoreClient.h"
30 class UrnState
: public StoreClient
32 CBDATA_CLASS(UrnState
);
35 explicit UrnState(const AccessLogEntry::Pointer
&anAle
): ale(anAle
) {}
37 void start (HttpRequest
*, StoreEntry
*);
38 void setUriResFromRequest(HttpRequest
*);
42 StoreEntry
*entry
= nullptr;
43 store_client
*sc
= nullptr;
44 StoreEntry
*urlres_e
= nullptr;
45 HttpRequest::Pointer request
;
46 HttpRequest::Pointer urlres_r
;
47 AccessLogEntry::Pointer ale
; ///< details of the requesting transaction
49 /// for receiving a URN resolver reply body from Store and interpreting it
50 Store::ParsingBuffer parsingBuffer
;
54 LogTags
*loggingTags() const override
{ return ale
? &ale
->cache
.code
: nullptr; }
55 void fillChecklist(ACLFilledChecklist
&) const override
;
57 char *urlres
= nullptr;
70 static STCB urnHandleReply
;
71 static url_entry
*urnParseReply(const SBuf
&, const HttpRequestMethod
&);
72 static const char *const crlf
= "\r\n";
74 CBDATA_CLASS_INIT(UrnState
);
81 storeUnregister(sc
, urlres_e
, this);
82 urlres_e
->unlock("~UrnState+res");
86 entry
->unlock("~UrnState+prime");
93 urnFindMinRtt(url_entry
* urls
, const HttpRequestMethod
&, int *rtt_ret
)
96 url_entry
*u
= nullptr;
97 url_entry
*min_u
= nullptr;
100 debugs(52, 3, "urnFindMinRtt");
101 assert(urls
!= nullptr);
103 for (i
= 0; nullptr != urls
[i
].url
; ++i
)
106 debugs(53, 3, "urnFindMinRtt: Counted " << i
<< " URLs");
109 debugs(52, 3, "urnFindMinRtt: Only one URL - return it!");
113 for (i
= 0; i
< urlcnt
; ++i
) {
115 debugs(52, 3, "urnFindMinRtt: " << u
->host
<< " rtt=" << u
->rtt
);
120 if (u
->rtt
> min_rtt
&& min_rtt
!= 0)
131 debugs(52, DBG_IMPORTANT
, "urnFindMinRtt: Returning '" <<
132 (min_u
? min_u
->url
: "NONE") << "' RTT " <<
139 UrnState::setUriResFromRequest(HttpRequest
*r
)
141 const auto &query
= r
->url
.absolute();
142 const auto host
= r
->url
.host();
143 // TODO: use class AnyP::Uri instead of generating a string and re-parsing
144 LOCAL_ARRAY(char, local_urlres
, 4096);
145 snprintf(local_urlres
, 4096, "http://%s/uri-res/N2L?" SQUIDSBUFPH
, host
, SQUIDSBUFPRINT(query
));
147 urlres_r
= HttpRequest::FromUrlXXX(local_urlres
, r
->masterXaction
);
150 debugs(52, 3, "Bad uri-res URL " << local_urlres
);
151 const auto err
= new ErrorState(ERR_URN_RESOLVE
, Http::scNotFound
, r
, ale
);
152 err
->url
= xstrdup(local_urlres
);
153 errorAppendEntry(entry
, err
);
157 urlres
= xstrdup(local_urlres
);
158 urlres_r
->header
.putStr(Http::HdrType::ACCEPT
, "text/plain");
162 UrnState::start(HttpRequest
* r
, StoreEntry
* e
)
164 debugs(52, 3, "urnStart: '" << e
->url() << "'" );
168 entry
->lock("UrnState::start");
169 setUriResFromRequest(r
);
171 if (urlres_r
== nullptr)
174 auto urlEntry
= storeGetPublic(urlres
, Http::METHOD_GET
);
176 if (!urlEntry
|| (urlEntry
->hittingRequiresCollapsing() && !startCollapsingOn(*urlEntry
, false))) {
177 urlres_e
= storeCreateEntry(urlres
, urlres
, RequestFlags(), Http::METHOD_GET
);
178 sc
= storeClientListAdd(urlres_e
, this);
179 FwdState::Start(Comm::ConnectionPointer(), urlres_e
, urlres_r
.getRaw(), ale
);
181 urlEntry
->abandon(__func__
);
186 urlres_e
->lock(__func__
);
187 sc
= storeClientListAdd(urlres_e
, this);
190 storeClientCopy(sc
, urlres_e
,
191 parsingBuffer
.makeInitialSpace(),
197 UrnState::fillChecklist(ACLFilledChecklist
&checklist
) const
199 checklist
.setRequest(request
.getRaw());
204 urnStart(HttpRequest
*r
, StoreEntry
*e
, const AccessLogEntryPointer
&ale
)
206 const auto anUrn
= new UrnState(ale
);
211 url_entry_sort(const void *A
, const void *B
)
213 const url_entry
*u1
= (const url_entry
*)A
;
214 const url_entry
*u2
= (const url_entry
*)B
;
216 if (u2
->rtt
== u1
->rtt
)
218 else if (0 == u1
->rtt
)
220 else if (0 == u2
->rtt
)
223 return u1
->rtt
- u2
->rtt
;
226 /* TODO: use the clientStream support for this */
228 urnHandleReply(void *data
, StoreIOBuffer result
)
230 UrnState
*urnState
= static_cast<UrnState
*>(data
);
231 StoreEntry
*e
= urnState
->entry
;
232 StoreEntry
*urlres_e
= urnState
->urlres_e
;
240 debugs(52, 3, result
<< " with " << *e
);
242 if (EBIT_TEST(urlres_e
->flags
, ENTRY_ABORTED
) || result
.flags
.error
) {
247 if (!e
->isAccepting()) {
248 debugs(52, 3, "terminating due to bad " << *e
);
253 urnState
->parsingBuffer
.appended(result
.data
, result
.length
);
255 /* If we haven't received the entire object (urn), copy more */
256 if (!urnState
->sc
->atEof()) {
257 const auto bufferedBytes
= urnState
->parsingBuffer
.contentSize();
258 const auto remainingSpace
= urnState
->parsingBuffer
.space().positionAt(bufferedBytes
);
260 if (!remainingSpace
.length
) {
261 debugs(52, 3, "ran out of buffer space after " << bufferedBytes
<< " bytes");
262 // TODO: Here and in other error cases, send ERR_URN_RESOLVE to client.
267 storeClientCopy(urnState
->sc
, urlres_e
,
274 const auto &peerReply
= urlres_e
->mem().baseReply();
275 debugs(52, 3, "got reply, code=" << peerReply
.sline
.status());
276 if (peerReply
.sline
.status() != Http::scOkay
) {
277 debugs(52, 3, "urnHandleReply: failed.");
278 err
= new ErrorState(ERR_URN_RESOLVE
, Http::scNotFound
, urnState
->request
.getRaw(), urnState
->ale
);
279 err
->url
= xstrdup(e
->url());
280 errorAppendEntry(e
, err
);
285 // XXX: Missing reply freshness checks (e.g., calling refreshCheckHTTP()).
287 urls
= urnParseReply(urnState
->parsingBuffer
.toSBuf(), urnState
->request
->method
);
289 if (!urls
) { /* unknown URN error */
290 debugs(52, 3, "urnTranslateDone: unknown URN " << e
->url());
291 err
= new ErrorState(ERR_URN_RESOLVE
, Http::scNotFound
, urnState
->request
.getRaw(), urnState
->ale
);
292 err
->url
= xstrdup(e
->url());
293 errorAppendEntry(e
, err
);
298 for (i
= 0; urls
[i
].url
; ++i
)
301 debugs(53, 3, "urnFindMinRtt: Counted " << i
<< " URLs");
303 min_u
= urnFindMinRtt(urls
, urnState
->request
->method
, nullptr);
304 qsort(urls
, urlcnt
, sizeof(*urls
), url_entry_sort
);
307 SBuf
*mb
= &body
; // diff reduction hack; TODO: Remove
308 mb
->appendf( "<TITLE>Select URL for %s</TITLE>\n"
309 "<STYLE type=\"text/css\"><!--BODY{background-color:#ffffff;font-family:verdana,sans-serif}--></STYLE>\n"
310 "<H2>Select URL for %s</H2>\n"
311 "<TABLE BORDER=\"0\" WIDTH=\"100%%\">\n", e
->url(), e
->url());
313 for (i
= 0; i
< urlcnt
; ++i
) {
315 debugs(52, 3, "URL {" << u
->url
<< "}");
317 "<TR><TD><A HREF=\"%s\">%s</A></TD>", u
->url
, u
->url
);
321 "<TD align=\"right\">%4d <it>ms</it></TD>", u
->rtt
);
323 mb
->appendf("<TD align=\"right\">Unknown</TD>");
325 mb
->appendf("<TD>%s</TD></TR>\n", u
->flags
.cached
? " [cached]" : " ");
330 "<HR noshade size=\"1px\">\n"
332 "Generated by %s@%s\n"
334 visible_appname_string
, getMyHostname());
335 const auto rep
= new HttpReply
;
336 rep
->setHeaders(Http::scFound
, nullptr, "text/html", mb
->length(), 0, squid_curtime
);
339 rep
->header
.putStr(Http::HdrType::LOCATION
, min_u
->url
);
343 e
->replaceHttpReply(rep
);
346 for (i
= 0; i
< urlcnt
; ++i
) {
347 safe_free(urls
[i
].url
);
348 safe_free(urls
[i
].host
);
357 urnParseReply(const SBuf
&inBuf
, const HttpRequestMethod
&m
)
364 debugs(52, 3, "urnParseReply");
365 list
= (url_entry
*)xcalloc(n
+ 1, sizeof(*list
));
367 // XXX: Switch to tokenizer-based parsing.
368 const auto allocated
= SBufToCstring(inBuf
);
370 auto buf
= allocated
;
371 while (xisspace(*buf
))
374 for (token
= strtok(buf
, crlf
); token
; token
= strtok(nullptr, crlf
)) {
375 debugs(52, 3, "urnParseReply: got '" << token
<< "'");
380 list
= (url_entry
*)xcalloc(n
+ 1, sizeof(*list
));
381 memcpy(list
, old
, i
* sizeof(*list
));
386 if (!uri
.parse(m
, SBuf(token
)) || !*uri
.host())
390 list
[i
].rtt
= netdbHostRtt(uri
.host());
392 if (0 == list
[i
].rtt
) {
393 debugs(52, 3, "Pinging " << uri
.host());
394 netdbPingSite(uri
.host());
400 list
[i
].url
= xstrdup(uri
.absolute().c_str());
401 list
[i
].host
= xstrdup(uri
.host());
402 // TODO: Use storeHas() or lock/unlock entry to avoid creating unlocked
404 list
[i
].flags
.cached
= storeGetPublic(list
[i
].url
, m
) ? 1 : 0;
408 debugs(52, 3, "urnParseReply: Found " << i
<< " URLs");