1 /*
2  * Copyright (C) 1996-2021 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 04    Error Generation */
10 
11 #include "squid.h"
12 #include "cache_cf.h"
13 #include "clients/forward.h"
14 #include "comm/Connection.h"
15 #include "comm/Write.h"
16 #include "err_detail_type.h"
17 #include "errorpage.h"
18 #include "fde.h"
19 #include "fs_io.h"
20 #include "html_quote.h"
21 #include "HttpHeaderTools.h"
22 #include "HttpReply.h"
23 #include "HttpRequest.h"
24 #include "MemBuf.h"
25 #include "MemObject.h"
26 #include "rfc1738.h"
27 #include "SquidConfig.h"
28 #include "Store.h"
29 #include "tools.h"
30 #include "wordlist.h"
31 #if USE_AUTH
32 #include "auth/UserRequest.h"
33 #endif
34 #include "SquidTime.h"
35 #if USE_OPENSSL
36 #include "ssl/ErrorDetailManager.h"
37 #endif
38 
39 /**
40  \defgroup ErrorPageInternal Error Page Internals
41  \ingroup ErrorPageAPI
42  *
43  \section Abstract Abstract:
44  *   These routines are used to generate error messages to be
45  *   sent to clients.  The error type is used to select between
46  *   the various message formats. (formats are stored in the
47  *   Config.errorDirectory)
48  */
49 
50 #if !defined(DEFAULT_SQUID_ERROR_DIR)
51 /** Where to look for errors if config path fails.
52  \note Please use ./configure --datadir=/path instead of patching
53  */
54 #define DEFAULT_SQUID_ERROR_DIR   DEFAULT_SQUID_DATA_DIR"/errors"
55 #endif
56 
57 /// \ingroup ErrorPageInternal
58 CBDATA_CLASS_INIT(ErrorState);
59 
60 /* local types */
61 
62 /// \ingroup ErrorPageInternal
63 typedef struct {
64     int id;
65     char *page_name;
66     Http::StatusCode page_redirect;
67 } ErrorDynamicPageInfo;
68 
69 /* local constant and vars */
70 
71 /**
72  \ingroup ErrorPageInternal
73  *
74  \note  hard coded error messages are not appended with %S
75  *      automagically to give you more control on the format
76  */
77 static const struct {
78     int type;           /* and page_id */
79     const char *text;
80 }
81 
82 error_hard_text[] = {
83 
84     {
85         ERR_SQUID_SIGNATURE,
86         "\n<br>\n"
87         "<hr>\n"
88         "<div id=\"footer\">\n"
89         "Generated %T by %h (%s)\n"
90         "</div>\n"
91         "</body></html>\n"
92     },
93     {
94         TCP_RESET,
95         "reset"
96     }
97 };
98 
99 /// \ingroup ErrorPageInternal
100 static std::vector<ErrorDynamicPageInfo *> ErrorDynamicPages;
101 
102 /* local prototypes */
103 
104 /// \ingroup ErrorPageInternal
105 static const int error_hard_text_count = sizeof(error_hard_text) / sizeof(*error_hard_text);
106 
107 /// \ingroup ErrorPageInternal
108 static char **error_text = NULL;
109 
110 /// \ingroup ErrorPageInternal
111 static int error_page_count = 0;
112 
113 /// \ingroup ErrorPageInternal
114 static MemBuf error_stylesheet;
115 
116 static const char *errorFindHardText(err_type type);
117 static ErrorDynamicPageInfo *errorDynamicPageInfoCreate(int id, const char *page_name);
118 static void errorDynamicPageInfoDestroy(ErrorDynamicPageInfo * info);
119 static IOCB errorSendComplete;
120 
121 /// \ingroup ErrorPageInternal
122 /// manages an error page template
123 class ErrorPageFile: public TemplateFile
124 {
125 public:
ErrorPageFile(const char * name,const err_type code)126     ErrorPageFile(const char *name, const err_type code) : TemplateFile(name,code) {textBuf.init();}
127 
128     /// The template text data read from disk
text()129     const char *text() { return textBuf.content(); }
130 
131 private:
132     /// stores the data read from disk to a local buffer
parse(const char * buf,int len,bool)133     virtual bool parse(const char *buf, int len, bool) {
134         if (len)
135             textBuf.append(buf, len);
136         return true;
137     }
138 
139     MemBuf textBuf; ///< A buffer to store the error page
140 };
141 
142 /// \ingroup ErrorPageInternal
operator ++(err_type & anErr)143 err_type &operator++ (err_type &anErr)
144 {
145     int tmp = (int)anErr;
146     anErr = (err_type)(++tmp);
147     return anErr;
148 }
149 
150 /// \ingroup ErrorPageInternal
operator -(err_type const & anErr,err_type const & anErr2)151 int operator - (err_type const &anErr, err_type const &anErr2)
152 {
153     return (int)anErr - (int)anErr2;
154 }
155 
156 void
errorInitialize(void)157 errorInitialize(void)
158 {
159     err_type i;
160     const char *text;
161     error_page_count = ERR_MAX + ErrorDynamicPages.size();
162     error_text = static_cast<char **>(xcalloc(error_page_count, sizeof(char *)));
163 
164     for (i = ERR_NONE, ++i; i < error_page_count; ++i) {
165         safe_free(error_text[i]);
166 
167         if ((text = errorFindHardText(i))) {
168             /**\par
169              * Index any hard-coded error text into defaults.
170              */
171             error_text[i] = xstrdup(text);
172 
173         } else if (i < ERR_MAX) {
174             /**\par
175              * Index precompiled fixed template files from one of two sources:
176              *  (a) default language translation directory (error_default_language)
177              *  (b) admin specified custom directory (error_directory)
178              */
179             ErrorPageFile errTmpl(err_type_str[i], i);
180             error_text[i] = errTmpl.loadDefault() ? xstrdup(errTmpl.text()) : NULL;
181         } else {
182             /** \par
183              * Index any unknown file names used by deny_info.
184              */
185             ErrorDynamicPageInfo *info = ErrorDynamicPages.at(i - ERR_MAX);
186             assert(info && info->id == i && info->page_name);
187 
188             const char *pg = info->page_name;
189             if (info->page_redirect != Http::scNone)
190                 pg = info->page_name +4;
191 
192             if (strchr(pg, ':') == NULL) {
193                 /** But only if they are not redirection URL. */
194                 ErrorPageFile errTmpl(pg, ERR_MAX);
195                 error_text[i] = errTmpl.loadDefault() ? xstrdup(errTmpl.text()) : NULL;
196             }
197         }
198     }
199 
200     error_stylesheet.reset();
201 
202     // look for and load stylesheet into global MemBuf for it.
203     if (Config.errorStylesheet) {
204         ErrorPageFile tmpl("StylesSheet", ERR_MAX);
205         tmpl.loadFromFile(Config.errorStylesheet);
206         error_stylesheet.appendf("%s",tmpl.text());
207     }
208 
209 #if USE_OPENSSL
210     Ssl::errorDetailInitialize();
211 #endif
212 }
213 
214 void
errorClean(void)215 errorClean(void)
216 {
217     if (error_text) {
218         int i;
219 
220         for (i = ERR_NONE + 1; i < error_page_count; ++i)
221             safe_free(error_text[i]);
222 
223         safe_free(error_text);
224     }
225 
226     while (!ErrorDynamicPages.empty()) {
227         errorDynamicPageInfoDestroy(ErrorDynamicPages.back());
228         ErrorDynamicPages.pop_back();
229     }
230 
231     error_page_count = 0;
232 
233 #if USE_OPENSSL
234     Ssl::errorDetailClean();
235 #endif
236 }
237 
238 /// \ingroup ErrorPageInternal
239 static const char *
errorFindHardText(err_type type)240 errorFindHardText(err_type type)
241 {
242     int i;
243 
244     for (i = 0; i < error_hard_text_count; ++i)
245         if (error_hard_text[i].type == type)
246             return error_hard_text[i].text;
247 
248     return NULL;
249 }
250 
TemplateFile(const char * name,const err_type code)251 TemplateFile::TemplateFile(const char *name, const err_type code): silent(false), wasLoaded(false), templateName(name), templateCode(code)
252 {
253     assert(name);
254 }
255 
256 bool
loadDefault()257 TemplateFile::loadDefault()
258 {
259     if (loaded()) // already loaded?
260         return true;
261 
262     /** test error_directory configured location */
263     if (Config.errorDirectory) {
264         char path[MAXPATHLEN];
265         snprintf(path, sizeof(path), "%s/%s", Config.errorDirectory, templateName.termedBuf());
266         loadFromFile(path);
267     }
268 
269 #if USE_ERR_LOCALES
270     /** test error_default_language location */
271     if (!loaded() && Config.errorDefaultLanguage) {
272         if (!tryLoadTemplate(Config.errorDefaultLanguage)) {
273             debugs(1, (templateCode < TCP_RESET ? DBG_CRITICAL : 3), "Unable to load default error language files. Reset to backups.");
274         }
275     }
276 #endif
277 
278     /* test default location if failed (templates == English translation base templates) */
279     if (!loaded()) {
280         tryLoadTemplate("templates");
281     }
282 
283     /* giving up if failed */
284     if (!loaded()) {
285         debugs(1, (templateCode < TCP_RESET ? DBG_CRITICAL : 3), "WARNING: failed to find or read error text file " << templateName);
286         parse("Internal Error: Missing Template ", 33, '\0');
287         parse(templateName.termedBuf(), templateName.size(), '\0');
288     }
289 
290     return true;
291 }
292 
293 bool
tryLoadTemplate(const char * lang)294 TemplateFile::tryLoadTemplate(const char *lang)
295 {
296     assert(lang);
297 
298     char path[MAXPATHLEN];
299     /* TODO: prep the directory path string to prevent snprintf ... */
300     snprintf(path, sizeof(path), "%s/%s/%s",
301              DEFAULT_SQUID_ERROR_DIR, lang, templateName.termedBuf());
302     path[MAXPATHLEN-1] = '\0';
303 
304     if (loadFromFile(path))
305         return true;
306 
307 #if HAVE_GLOB
308     if ( strlen(lang) == 2) {
309         /* TODO glob the error directory for sub-dirs matching: <tag> '-*'   */
310         /* use first result. */
311         debugs(4,2, HERE << "wildcard fallback errors not coded yet.");
312     }
313 #endif
314 
315     return false;
316 }
317 
318 bool
loadFromFile(const char * path)319 TemplateFile::loadFromFile(const char *path)
320 {
321     int fd;
322     char buf[4096];
323     ssize_t len;
324 
325     if (loaded()) // already loaded?
326         return true;
327 
328     fd = file_open(path, O_RDONLY | O_TEXT);
329 
330     if (fd < 0) {
331         /* with dynamic locale negotiation we may see some failures before a success. */
332         if (!silent && templateCode < TCP_RESET) {
333             int xerrno = errno;
334             debugs(4, DBG_CRITICAL, "ERROR: loading file '" << path << "': " << xstrerr(xerrno));
335         }
336         wasLoaded = false;
337         return wasLoaded;
338     }
339 
340     while ((len = FD_READ_METHOD(fd, buf, sizeof(buf))) > 0) {
341         if (!parse(buf, len, false)) {
342             debugs(4, DBG_CRITICAL, "ERROR: parsing error in template file: " << path);
343             wasLoaded = false;
344             return wasLoaded;
345         }
346     }
347     parse(buf, 0, true);
348 
349     if (len < 0) {
350         int xerrno = errno;
351         debugs(4, DBG_CRITICAL, MYNAME << "ERROR: failed to fully read: '" << path << "': " << xstrerr(xerrno));
352     }
353 
354     file_close(fd);
355 
356     wasLoaded = true;
357     return wasLoaded;
358 }
359 
strHdrAcptLangGetItem(const String & hdr,char * lang,int langLen,size_t & pos)360 bool strHdrAcptLangGetItem(const String &hdr, char *lang, int langLen, size_t &pos)
361 {
362     while (pos < hdr.size()) {
363 
364         /* skip any initial whitespace. */
365         while (pos < hdr.size() && xisspace(hdr[pos]))
366             ++pos;
367 
368         /*
369          * Header value format:
370          *  - sequence of whitespace delimited tags
371          *  - each tag may suffix with ';'.* which we can ignore.
372          *  - IFF a tag contains only two characters we can wildcard ANY translations matching: <it> '-'? .*
373          *    with preference given to an exact match.
374          */
375         bool invalid_byte = false;
376         char *dt = lang;
377         while (pos < hdr.size() && hdr[pos] != ';' && hdr[pos] != ',' && !xisspace(hdr[pos]) && dt < (lang + (langLen -1)) ) {
378             if (!invalid_byte) {
379 #if USE_HTTP_VIOLATIONS
380                 // if accepting violations we may as well accept some broken browsers
381                 //  which may send us the right code, wrong ISO formatting.
382                 if (hdr[pos] == '_')
383                     *dt = '-';
384                 else
385 #endif
386                     *dt = xtolower(hdr[pos]);
387                 // valid codes only contain A-Z, hyphen (-) and *
388                 if (*dt != '-' && *dt != '*' && (*dt < 'a' || *dt > 'z') )
389                     invalid_byte = true;
390                 else
391                     ++dt; // move to next destination byte.
392             }
393             ++pos;
394         }
395         *dt = '\0'; // nul-terminated the filename content string before system use.
396 
397         // if we terminated the tag on garbage or ';' we need to skip to the next ',' or end of header.
398         while (pos < hdr.size() && hdr[pos] != ',')
399             ++pos;
400 
401         if (pos < hdr.size() && hdr[pos] == ',')
402             ++pos;
403 
404         debugs(4, 9, "STATE: lang=" << lang << ", pos=" << pos << ", buf='" << ((pos < hdr.size()) ? hdr.substr(pos,hdr.size()) : "") << "'");
405 
406         /* if we found anything we might use, try it. */
407         if (*lang != '\0' && !invalid_byte)
408             return true;
409     }
410     return false;
411 }
412 
413 bool
loadFor(const HttpRequest * request)414 TemplateFile::loadFor(const HttpRequest *request)
415 {
416     String hdr;
417 
418 #if USE_ERR_LOCALES
419     if (loaded()) // already loaded?
420         return true;
421 
422     if (!request || !request->header.getList(Http::HdrType::ACCEPT_LANGUAGE, &hdr) )
423         return false;
424 
425     char lang[256];
426     size_t pos = 0; // current parsing position in header string
427 
428     debugs(4, 6, HERE << "Testing Header: '" << hdr << "'");
429 
430     while ( strHdrAcptLangGetItem(hdr, lang, 256, pos) ) {
431 
432         /* wildcard uses the configured default language */
433         if (lang[0] == '*' && lang[1] == '\0') {
434             debugs(4, 6, HERE << "Found language '" << lang << "'. Using configured default.");
435             return false;
436         }
437 
438         debugs(4, 6, HERE << "Found language '" << lang << "', testing for available template");
439 
440         if (tryLoadTemplate(lang)) {
441             /* store the language we found for the Content-Language reply header */
442             errLanguage = lang;
443             break;
444         } else if (Config.errorLogMissingLanguages) {
445             debugs(4, DBG_IMPORTANT, "WARNING: Error Pages Missing Language: " << lang);
446         }
447     }
448 #endif
449 
450     return loaded();
451 }
452 
453 /// \ingroup ErrorPageInternal
454 static ErrorDynamicPageInfo *
errorDynamicPageInfoCreate(int id,const char * page_name)455 errorDynamicPageInfoCreate(int id, const char *page_name)
456 {
457     ErrorDynamicPageInfo *info = new ErrorDynamicPageInfo;
458     info->id = id;
459     info->page_name = xstrdup(page_name);
460     info->page_redirect = static_cast<Http::StatusCode>(atoi(page_name));
461 
462     /* WARNING on redirection status:
463      * 2xx are permitted, but not documented officially.
464      * - might be useful for serving static files (PAC etc) in special cases
465      * 3xx require a URL suitable for Location: header.
466      * - the current design does not allow for a Location: URI as well as a local file template
467      *   although this possibility is explicitly permitted in the specs.
468      * 4xx-5xx require a local file template.
469      * - sending Location: on these codes with no body is invalid by the specs.
470      * - current result is Squid crashing or XSS problems as dynamic deny_info load random disk files.
471      * - a future redesign of the file loading may result in loading remote objects sent inline as local body.
472      */
473     if (info->page_redirect == Http::scNone)
474         ; // special case okay.
475     else if (info->page_redirect < 200 || info->page_redirect > 599) {
476         // out of range
477         debugs(0, DBG_CRITICAL, "FATAL: status " << info->page_redirect << " is not valid on '" << page_name << "'");
478         self_destruct();
479     } else if ( /* >= 200 && */ info->page_redirect < 300 && strchr(&(page_name[4]), ':')) {
480         // 2xx require a local template file
481         debugs(0, DBG_CRITICAL, "FATAL: status " << info->page_redirect << " requires a template on '" << page_name << "'");
482         self_destruct();
483     } else if (info->page_redirect >= 300 && info->page_redirect <= 399 && !strchr(&(page_name[4]), ':')) {
484         // 3xx require an absolute URL
485         debugs(0, DBG_CRITICAL, "FATAL: status " << info->page_redirect << " requires a URL on '" << page_name << "'");
486         self_destruct();
487     } else if (info->page_redirect >= 400 /* && <= 599 */ && strchr(&(page_name[4]), ':')) {
488         // 4xx/5xx require a local template file
489         debugs(0, DBG_CRITICAL, "FATAL: status " << info->page_redirect << " requires a template on '" << page_name << "'");
490         self_destruct();
491     }
492     // else okay.
493 
494     return info;
495 }
496 
497 /// \ingroup ErrorPageInternal
498 static void
errorDynamicPageInfoDestroy(ErrorDynamicPageInfo * info)499 errorDynamicPageInfoDestroy(ErrorDynamicPageInfo * info)
500 {
501     assert(info);
502     safe_free(info->page_name);
503     delete info;
504 }
505 
506 /// \ingroup ErrorPageInternal
507 static int
errorPageId(const char * page_name)508 errorPageId(const char *page_name)
509 {
510     for (int i = 0; i < ERR_MAX; ++i) {
511         if (strcmp(err_type_str[i], page_name) == 0)
512             return i;
513     }
514 
515     for (size_t j = 0; j < ErrorDynamicPages.size(); ++j) {
516         if (strcmp(ErrorDynamicPages[j]->page_name, page_name) == 0)
517             return j + ERR_MAX;
518     }
519 
520     return ERR_NONE;
521 }
522 
523 err_type
errorReservePageId(const char * page_name)524 errorReservePageId(const char *page_name)
525 {
526     ErrorDynamicPageInfo *info;
527     int id = errorPageId(page_name);
528 
529     if (id == ERR_NONE) {
530         info = errorDynamicPageInfoCreate(ERR_MAX + ErrorDynamicPages.size(), page_name);
531         ErrorDynamicPages.push_back(info);
532         id = info->id;
533     }
534 
535     return (err_type)id;
536 }
537 
538 /// \ingroup ErrorPageInternal
539 const char *
errorPageName(int pageId)540 errorPageName(int pageId)
541 {
542     if (pageId >= ERR_NONE && pageId < ERR_MAX)     /* common case */
543         return err_type_str[pageId];
544 
545     if (pageId >= ERR_MAX && pageId - ERR_MAX < (ssize_t)ErrorDynamicPages.size())
546         return ErrorDynamicPages[pageId - ERR_MAX]->page_name;
547 
548     return "ERR_UNKNOWN";   /* should not happen */
549 }
550 
551 ErrorState *
NewForwarding(err_type type,HttpRequest * request)552 ErrorState::NewForwarding(err_type type, HttpRequest *request)
553 {
554     assert(request);
555     const Http::StatusCode status = request->flags.needValidation ?
556                                     Http::scGatewayTimeout : Http::scServiceUnavailable;
557     return new ErrorState(type, status, request);
558 }
559 
ErrorState(err_type t,Http::StatusCode status,HttpRequest * req)560 ErrorState::ErrorState(err_type t, Http::StatusCode status, HttpRequest * req) :
561     type(t),
562     page_id(t),
563     err_language(NULL),
564     httpStatus(status),
565 #if USE_AUTH
566     auth_user_request (NULL),
567 #endif
568     request(NULL),
569     url(NULL),
570     xerrno(0),
571     port(0),
572     dnsError(),
573     ttl(0),
574     src_addr(),
575     redirect_url(NULL),
576     callback(NULL),
577     callback_data(NULL),
578     request_hdrs(NULL),
579     err_msg(NULL),
580 #if USE_OPENSSL
581     detail(NULL),
582 #endif
583     detailCode(ERR_DETAIL_NONE)
584 {
585     memset(&ftp, 0, sizeof(ftp));
586 
587     if (page_id >= ERR_MAX && ErrorDynamicPages[page_id - ERR_MAX]->page_redirect != Http::scNone)
588         httpStatus = ErrorDynamicPages[page_id - ERR_MAX]->page_redirect;
589 
590     if (req != NULL) {
591         request = req;
592         HTTPMSGLOCK(request);
593         src_addr = req->client_addr;
594     }
595 }
596 
597 void
errorAppendEntry(StoreEntry * entry,ErrorState * err)598 errorAppendEntry(StoreEntry * entry, ErrorState * err)
599 {
600     assert(entry->mem_obj != NULL);
601     assert (entry->isEmpty());
602     debugs(4, 4, "Creating an error page for entry " << entry <<
603            " with errorstate " << err <<
604            " page id " << err->page_id);
605 
606     if (entry->store_status != STORE_PENDING) {
607         debugs(4, 2, "Skipping error page due to store_status: " << entry->store_status);
608         /*
609          * If the entry is not STORE_PENDING, then no clients
610          * care about it, and we don't need to generate an
611          * error message
612          */
613         assert(EBIT_TEST(entry->flags, ENTRY_ABORTED));
614         assert(entry->mem_obj->nclients == 0);
615         delete err;
616         return;
617     }
618 
619     if (err->page_id == TCP_RESET) {
620         if (err->request) {
621             debugs(4, 2, "RSTing this reply");
622             err->request->flags.resetTcp = true;
623         }
624     }
625 
626     entry->storeErrorResponse(err->BuildHttpReply());
627     delete err;
628 }
629 
630 void
errorSend(const Comm::ConnectionPointer & conn,ErrorState * err)631 errorSend(const Comm::ConnectionPointer &conn, ErrorState * err)
632 {
633     HttpReply *rep;
634     debugs(4, 3, HERE << conn << ", err=" << err);
635     assert(Comm::IsConnOpen(conn));
636 
637     rep = err->BuildHttpReply();
638 
639     MemBuf *mb = rep->pack();
640     AsyncCall::Pointer call = commCbCall(78, 5, "errorSendComplete",
641                                          CommIoCbPtrFun(&errorSendComplete, err));
642     Comm::Write(conn, mb, call);
643     delete mb;
644 
645     delete rep;
646 }
647 
648 /**
649  \ingroup ErrorPageAPI
650  *
651  * Called by commHandleWrite() after data has been written
652  * to the client socket.
653  *
654  \note If there is a callback, the callback is responsible for
655  *     closing the FD, otherwise we do it ourselves.
656  */
657 static void
errorSendComplete(const Comm::ConnectionPointer & conn,char *,size_t size,Comm::Flag errflag,int,void * data)658 errorSendComplete(const Comm::ConnectionPointer &conn, char *, size_t size, Comm::Flag errflag, int, void *data)
659 {
660     ErrorState *err = static_cast<ErrorState *>(data);
661     debugs(4, 3, HERE << conn << ", size=" << size);
662 
663     if (errflag != Comm::ERR_CLOSING) {
664         if (err->callback) {
665             debugs(4, 3, "errorSendComplete: callback");
666             err->callback(conn->fd, err->callback_data, size);
667         } else {
668             debugs(4, 3, "errorSendComplete: comm_close");
669             conn->close();
670         }
671     }
672 
673     delete err;
674 }
675 
~ErrorState()676 ErrorState::~ErrorState()
677 {
678     HTTPMSGUNLOCK(request);
679     safe_free(redirect_url);
680     safe_free(url);
681     safe_free(request_hdrs);
682     wordlistDestroy(&ftp.server_msg);
683     safe_free(ftp.request);
684     safe_free(ftp.reply);
685 #if USE_AUTH
686     auth_user_request = NULL;
687 #endif
688     safe_free(err_msg);
689 #if USE_ERR_LOCALES
690     if (err_language != Config.errorDefaultLanguage)
691 #endif
692         safe_free(err_language);
693 #if USE_OPENSSL
694     delete detail;
695 #endif
696 }
697 
698 int
Dump(MemBuf * mb)699 ErrorState::Dump(MemBuf * mb)
700 {
701     MemBuf str;
702     char ntoabuf[MAX_IPSTRLEN];
703 
704     str.reset();
705     /* email subject line */
706     str.appendf("CacheErrorInfo - %s", errorPageName(type));
707     mb->appendf("?subject=%s", rfc1738_escape_part(str.buf));
708     str.reset();
709     /* email body */
710     str.appendf("CacheHost: %s\r\n", getMyHostname());
711     /* - Err Msgs */
712     str.appendf("ErrPage: %s\r\n", errorPageName(type));
713 
714     if (xerrno) {
715         str.appendf("Err: (%d) %s\r\n", xerrno, strerror(xerrno));
716     } else {
717         str.append("Err: [none]\r\n", 13);
718     }
719 #if USE_AUTH
720     if (auth_user_request.getRaw() && auth_user_request->denyMessage())
721         str.appendf("Auth ErrMsg: %s\r\n", auth_user_request->denyMessage());
722 #endif
723     if (dnsError.size() > 0)
724         str.appendf("DNS ErrMsg: %s\r\n", dnsError.termedBuf());
725 
726     /* - TimeStamp */
727     str.appendf("TimeStamp: %s\r\n\r\n", mkrfc1123(squid_curtime));
728 
729     /* - IP stuff */
730     str.appendf("ClientIP: %s\r\n", src_addr.toStr(ntoabuf,MAX_IPSTRLEN));
731 
732     if (request && request->hier.host[0] != '\0') {
733         str.appendf("ServerIP: %s\r\n", request->hier.host);
734     }
735 
736     str.append("\r\n", 2);
737     /* - HTTP stuff */
738     str.append("HTTP Request:\r\n", 15);
739     if (request) {
740         str.appendf(SQUIDSBUFPH " " SQUIDSBUFPH " %s/%d.%d\n",
741                     SQUIDSBUFPRINT(request->method.image()),
742                     SQUIDSBUFPRINT(request->url.path()),
743                     AnyP::ProtocolType_str[request->http_ver.protocol],
744                     request->http_ver.major, request->http_ver.minor);
745         request->header.packInto(&str);
746     }
747 
748     str.append("\r\n", 2);
749     /* - FTP stuff */
750 
751     if (ftp.request) {
752         str.appendf("FTP Request: %s\r\n", ftp.request);
753         str.appendf("FTP Reply: %s\r\n", (ftp.reply? ftp.reply:"[none]"));
754         str.append("FTP Msg: ", 9);
755         wordlistCat(ftp.server_msg, &str);
756         str.append("\r\n", 2);
757     }
758 
759     str.append("\r\n", 2);
760     mb->appendf("&body=%s", rfc1738_escape_part(str.buf));
761     str.clean();
762     return 0;
763 }
764 
765 /// \ingroup ErrorPageInternal
766 #define CVT_BUF_SZ 512
767 
768 const char *
Convert(char token,bool building_deny_info_url,bool allowRecursion)769 ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion)
770 {
771     static MemBuf mb;
772     const char *p = NULL;   /* takes priority over mb if set */
773     int do_quote = 1;
774     int no_urlescape = 0;       /* if true then item is NOT to be further URL-encoded */
775     char ntoabuf[MAX_IPSTRLEN];
776 
777     mb.reset();
778 
779     switch (token) {
780 
781     case 'a':
782 #if USE_AUTH
783         if (request && request->auth_user_request != NULL)
784             p = request->auth_user_request->username();
785         if (!p)
786 #endif
787             p = "-";
788         break;
789 
790     case 'b':
791         mb.appendf("%u", getMyPort());
792         break;
793 
794     case 'B':
795         if (building_deny_info_url) break;
796         if (request) {
797             const SBuf &tmp = Ftp::UrlWith2f(request);
798             mb.append(tmp.rawContent(), tmp.length());
799         } else
800             p = "[no URL]";
801         break;
802 
803     case 'c':
804         if (building_deny_info_url) break;
805         p = errorPageName(type);
806         break;
807 
808     case 'D':
809         if (!allowRecursion)
810             p = "%D";  // if recursion is not allowed, do not convert
811 #if USE_OPENSSL
812         // currently only SSL error details implemented
813         else if (detail) {
814             detail->useRequest(request);
815             const String &errDetail = detail->toString();
816             if (errDetail.size() > 0) {
817                 MemBuf *detail_mb  = ConvertText(errDetail.termedBuf(), false);
818                 mb.append(detail_mb->content(), detail_mb->contentSize());
819                 delete detail_mb;
820                 do_quote = 0;
821             }
822         }
823 #endif
824         if (!mb.contentSize())
825             mb.append("[No Error Detail]", 17);
826         break;
827 
828     case 'e':
829         mb.appendf("%d", xerrno);
830         break;
831 
832     case 'E':
833         if (xerrno)
834             mb.appendf("(%d) %s", xerrno, strerror(xerrno));
835         else
836             mb.append("[No Error]", 10);
837         break;
838 
839     case 'f':
840         if (building_deny_info_url) break;
841         /* FTP REQUEST LINE */
842         if (ftp.request)
843             p = ftp.request;
844         else
845             p = "nothing";
846         break;
847 
848     case 'F':
849         if (building_deny_info_url) break;
850         /* FTP REPLY LINE */
851         if (ftp.reply)
852             p = ftp.reply;
853         else
854             p = "nothing";
855         break;
856 
857     case 'g':
858         if (building_deny_info_url) break;
859         /* FTP SERVER RESPONSE */
860         if (ftp.listing) {
861             mb.append(ftp.listing->content(), ftp.listing->contentSize());
862             do_quote = 0;
863         } else if (ftp.server_msg) {
864             wordlistCat(ftp.server_msg, &mb);
865         }
866         break;
867 
868     case 'h':
869         mb.appendf("%s", getMyHostname());
870         break;
871 
872     case 'H':
873         if (request) {
874             if (request->hier.host[0] != '\0') // if non-empty string.
875                 p = request->hier.host;
876             else
877                 p = request->url.host();
878         } else if (!building_deny_info_url)
879             p = "[unknown host]";
880         break;
881 
882     case 'i':
883         mb.appendf("%s", src_addr.toStr(ntoabuf,MAX_IPSTRLEN));
884         break;
885 
886     case 'I':
887         if (request && request->hier.tcpServer != NULL)
888             p = request->hier.tcpServer->remote.toStr(ntoabuf,MAX_IPSTRLEN);
889         else if (!building_deny_info_url)
890             p = "[unknown]";
891         break;
892 
893     case 'l':
894         if (building_deny_info_url) break;
895         mb.append(error_stylesheet.content(), error_stylesheet.contentSize());
896         do_quote = 0;
897         break;
898 
899     case 'L':
900         if (building_deny_info_url) break;
901         if (Config.errHtmlText) {
902             mb.appendf("%s", Config.errHtmlText);
903             do_quote = 0;
904         } else
905             p = "[not available]";
906         break;
907 
908     case 'm':
909         if (building_deny_info_url) break;
910 #if USE_AUTH
911         if (auth_user_request.getRaw())
912             p = auth_user_request->denyMessage("[not available]");
913         else
914             p = "[not available]";
915 #else
916         p = "-";
917 #endif
918         break;
919 
920     case 'M':
921         if (request) {
922             const SBuf &m = request->method.image();
923             mb.append(m.rawContent(), m.length());
924         } else if (!building_deny_info_url)
925             p = "[unknown method]";
926         break;
927 
928     case 'O':
929         if (!building_deny_info_url)
930             do_quote = 0;
931     case 'o':
932         p = request ? request->extacl_message.termedBuf() : external_acl_message;
933         if (!p && !building_deny_info_url)
934             p = "[not available]";
935         break;
936 
937     case 'p':
938         if (request) {
939             mb.appendf("%u", request->url.port());
940         } else if (!building_deny_info_url) {
941             p = "[unknown port]";
942         }
943         break;
944 
945     case 'P':
946         if (request) {
947             const SBuf &m = request->url.getScheme().image();
948             mb.append(m.rawContent(), m.length());
949         } else if (!building_deny_info_url) {
950             p = "[unknown protocol]";
951         }
952         break;
953 
954     case 'R':
955         if (building_deny_info_url) {
956             if (request != NULL) {
957                 const SBuf &tmp = request->url.path();
958                 mb.append(tmp.rawContent(), tmp.length());
959                 no_urlescape = 1;
960             } else
961                 p = "[no request]";
962             break;
963         }
964         if (request != NULL) {
965             mb.appendf(SQUIDSBUFPH " " SQUIDSBUFPH " %s/%d.%d\n",
966                        SQUIDSBUFPRINT(request->method.image()),
967                        SQUIDSBUFPRINT(request->url.path()),
968                        AnyP::ProtocolType_str[request->http_ver.protocol],
969                        request->http_ver.major, request->http_ver.minor);
970             request->header.packInto(&mb, true); //hide authorization data
971         } else if (request_hdrs) {
972             p = request_hdrs;
973         } else {
974             p = "[no request]";
975         }
976         break;
977 
978     case 's':
979         /* for backward compat we make %s show the full URL. Drop this in some future release. */
980         if (building_deny_info_url) {
981             if (request) {
982                 const SBuf &tmp = request->effectiveRequestUri();
983                 mb.append(tmp.rawContent(), tmp.length());
984             } else
985                 p = url;
986             debugs(0, DBG_CRITICAL, "WARNING: deny_info now accepts coded tags. Use %u to get the full URL instead of %s");
987         } else
988             p = visible_appname_string;
989         break;
990 
991     case 'S':
992         if (building_deny_info_url) {
993             p = visible_appname_string;
994             break;
995         }
996         /* signature may contain %-escapes, recursion */
997         if (page_id != ERR_SQUID_SIGNATURE) {
998             const int saved_id = page_id;
999             page_id = ERR_SQUID_SIGNATURE;
1000             MemBuf *sign_mb = BuildContent();
1001             mb.append(sign_mb->content(), sign_mb->contentSize());
1002             sign_mb->clean();
1003             delete sign_mb;
1004             page_id = saved_id;
1005             do_quote = 0;
1006         } else {
1007             /* wow, somebody put %S into ERR_SIGNATURE, stop recursion */
1008             p = "[%S]";
1009         }
1010         break;
1011 
1012     case 't':
1013         mb.appendf("%s", Time::FormatHttpd(squid_curtime));
1014         break;
1015 
1016     case 'T':
1017         mb.appendf("%s", mkrfc1123(squid_curtime));
1018         break;
1019 
1020     case 'U':
1021         /* Using the fake-https version of absolute-URI so error pages see https:// */
1022         /* even when the url-path cannot be shown as more than '*' */
1023         if (request)
1024             p = urlCanonicalFakeHttps(request);
1025         else if (url)
1026             p = url;
1027         else if (!building_deny_info_url)
1028             p = "[no URL]";
1029         break;
1030 
1031     case 'u':
1032         if (request) {
1033             const SBuf &tmp = request->effectiveRequestUri();
1034             mb.append(tmp.rawContent(), tmp.length());
1035         } else if (url)
1036             p = url;
1037         else if (!building_deny_info_url)
1038             p = "[no URL]";
1039         break;
1040 
1041     case 'w':
1042         if (Config.adminEmail)
1043             mb.appendf("%s", Config.adminEmail);
1044         else if (!building_deny_info_url)
1045             p = "[unknown]";
1046         break;
1047 
1048     case 'W':
1049         if (building_deny_info_url) break;
1050         if (Config.adminEmail && Config.onoff.emailErrData)
1051             Dump(&mb);
1052         no_urlescape = 1;
1053         break;
1054 
1055     case 'x':
1056 #if USE_OPENSSL
1057         if (detail)
1058             mb.appendf("%s", detail->errorName());
1059         else
1060 #endif
1061             if (!building_deny_info_url)
1062                 p = "[Unknown Error Code]";
1063         break;
1064 
1065     case 'z':
1066         if (building_deny_info_url) break;
1067         if (dnsError.size() > 0)
1068             p = dnsError.termedBuf();
1069         else if (ftp.cwd_msg)
1070             p = ftp.cwd_msg;
1071         else
1072             p = "[unknown]";
1073         break;
1074 
1075     case 'Z':
1076         if (building_deny_info_url) break;
1077         if (err_msg)
1078             p = err_msg;
1079         else
1080             p = "[unknown]";
1081         break;
1082 
1083     case '%':
1084         p = "%";
1085         break;
1086 
1087     default:
1088         mb.appendf("%%%c", token);
1089         do_quote = 0;
1090         break;
1091     }
1092 
1093     if (!p)
1094         p = mb.buf;     /* do not use mb after this assignment! */
1095 
1096     assert(p);
1097 
1098     debugs(4, 3, "errorConvert: %%" << token << " --> '" << p << "'" );
1099 
1100     if (do_quote)
1101         p = html_quote(p);
1102 
1103     if (building_deny_info_url && !no_urlescape)
1104         p = rfc1738_escape_part(p);
1105 
1106     return p;
1107 }
1108 
1109 void
DenyInfoLocation(const char * name,HttpRequest *,MemBuf & result)1110 ErrorState::DenyInfoLocation(const char *name, HttpRequest *, MemBuf &result)
1111 {
1112     char const *m = name;
1113     char const *p = m;
1114     char const *t;
1115 
1116     if (m[0] == '3')
1117         m += 4; // skip "3xx:"
1118 
1119     while ((p = strchr(m, '%'))) {
1120         result.append(m, p - m);       /* copy */
1121         t = Convert(*++p, true, true);       /* convert */
1122         result.appendf("%s", t);        /* copy */
1123         m = p + 1;                     /* advance */
1124     }
1125 
1126     if (*m)
1127         result.appendf("%s", m);        /* copy tail */
1128 
1129     assert((size_t)result.contentSize() == strlen(result.content()));
1130 }
1131 
1132 HttpReply *
BuildHttpReply()1133 ErrorState::BuildHttpReply()
1134 {
1135     HttpReply *rep = new HttpReply;
1136     const char *name = errorPageName(page_id);
1137     /* no LMT for error pages; error pages expire immediately */
1138 
1139     if (name[0] == '3' || (name[0] != '2' && name[0] != '4' && name[0] != '5' && strchr(name, ':'))) {
1140         /* Redirection */
1141         Http::StatusCode status = Http::scFound;
1142         // Use configured 3xx reply status if set.
1143         if (name[0] == '3')
1144             status = httpStatus;
1145         else {
1146             // Use 307 for HTTP/1.1 non-GET/HEAD requests.
1147             if (request != NULL && request->method != Http::METHOD_GET && request->method != Http::METHOD_HEAD && request->http_ver >= Http::ProtocolVersion(1,1))
1148                 status = Http::scTemporaryRedirect;
1149         }
1150 
1151         rep->setHeaders(status, NULL, "text/html;charset=utf-8", 0, 0, -1);
1152 
1153         if (request) {
1154             MemBuf redirect_location;
1155             redirect_location.init();
1156             DenyInfoLocation(name, request, redirect_location);
1157             httpHeaderPutStrf(&rep->header, Http::HdrType::LOCATION, "%s", redirect_location.content() );
1158         }
1159 
1160         httpHeaderPutStrf(&rep->header, Http::HdrType::X_SQUID_ERROR, "%d %s", httpStatus, "Access Denied");
1161     } else {
1162         MemBuf *content = BuildContent();
1163         rep->setHeaders(httpStatus, NULL, "text/html;charset=utf-8", content->contentSize(), 0, -1);
1164         /*
1165          * include some information for downstream caches. Implicit
1166          * replaceable content. This isn't quite sufficient. xerrno is not
1167          * necessarily meaningful to another system, so we really should
1168          * expand it. Additionally, we should identify ourselves. Someone
1169          * might want to know. Someone _will_ want to know OTOH, the first
1170          * X-CACHE-MISS entry should tell us who.
1171          */
1172         httpHeaderPutStrf(&rep->header, Http::HdrType::X_SQUID_ERROR, "%s %d", name, xerrno);
1173 
1174 #if USE_ERR_LOCALES
1175         /*
1176          * If error page auto-negotiate is enabled in any way, send the Vary.
1177          * RFC 2616 section 13.6 and 14.44 says MAY and SHOULD do this.
1178          * We have even better reasons though:
1179          * see http://wiki.squid-cache.org/KnowledgeBase/VaryNotCaching
1180          */
1181         if (!Config.errorDirectory) {
1182             /* We 'negotiated' this ONLY from the Accept-Language. */
1183             rep->header.delById(Http::HdrType::VARY);
1184             rep->header.putStr(Http::HdrType::VARY, "Accept-Language");
1185         }
1186 
1187         /* add the Content-Language header according to RFC section 14.12 */
1188         if (err_language) {
1189             rep->header.putStr(Http::HdrType::CONTENT_LANGUAGE, err_language);
1190         } else
1191 #endif /* USE_ERROR_LOCALES */
1192         {
1193             /* default templates are in English */
1194             /* language is known unless error_directory override used */
1195             if (!Config.errorDirectory)
1196                 rep->header.putStr(Http::HdrType::CONTENT_LANGUAGE, "en");
1197         }
1198 
1199         rep->body.setMb(content);
1200         /* do not memBufClean() or delete the content, it was absorbed by httpBody */
1201     }
1202 
1203     // Make sure error codes get back to the client side for logging and
1204     // error tracking.
1205     if (request) {
1206         int edc = ERR_DETAIL_NONE; // error detail code
1207 #if USE_OPENSSL
1208         if (detail)
1209             edc = detail->errorNo();
1210         else
1211 #endif
1212             if (detailCode)
1213                 edc = detailCode;
1214             else
1215                 edc = xerrno;
1216         request->detailError(type, edc);
1217     }
1218 
1219     return rep;
1220 }
1221 
1222 MemBuf *
BuildContent()1223 ErrorState::BuildContent()
1224 {
1225     const char *m = NULL;
1226 
1227     assert(page_id > ERR_NONE && page_id < error_page_count);
1228 
1229 #if USE_ERR_LOCALES
1230     ErrorPageFile *localeTmpl = NULL;
1231 
1232     /** error_directory option in squid.conf overrides translations.
1233      * Custom errors are always found either in error_directory or the templates directory.
1234      * Otherwise locate the Accept-Language header
1235      */
1236     if (!Config.errorDirectory && page_id < ERR_MAX) {
1237         if (err_language && err_language != Config.errorDefaultLanguage)
1238             safe_free(err_language);
1239 
1240         localeTmpl = new ErrorPageFile(err_type_str[page_id], static_cast<err_type>(page_id));
1241         if (localeTmpl->loadFor(request)) {
1242             m = localeTmpl->text();
1243             assert(localeTmpl->language());
1244             err_language = xstrdup(localeTmpl->language());
1245         }
1246     }
1247 #endif /* USE_ERR_LOCALES */
1248 
1249     /** \par
1250      * If client-specific error templates are not enabled or available.
1251      * fall back to the old style squid.conf settings.
1252      */
1253     if (!m) {
1254         m = error_text[page_id];
1255 #if USE_ERR_LOCALES
1256         if (!Config.errorDirectory)
1257             err_language = Config.errorDefaultLanguage;
1258 #endif
1259         debugs(4, 2, HERE << "No existing error page language negotiated for " << errorPageName(page_id) << ". Using default error file.");
1260     }
1261 
1262     MemBuf *result = ConvertText(m, true);
1263 #if USE_ERR_LOCALES
1264     if (localeTmpl)
1265         delete localeTmpl;
1266 #endif
1267     return result;
1268 }
1269 
ConvertText(const char * text,bool allowRecursion)1270 MemBuf *ErrorState::ConvertText(const char *text, bool allowRecursion)
1271 {
1272     MemBuf *content = new MemBuf;
1273     const char *p;
1274     const char *m = text;
1275     assert(m);
1276     content->init();
1277 
1278     while ((p = strchr(m, '%'))) {
1279         content->append(m, p - m);  /* copy */
1280         const char *t = Convert(*++p, false, allowRecursion);   /* convert */
1281         content->appendf("%s", t);   /* copy */
1282         m = p + 1;          /* advance */
1283     }
1284 
1285     if (*m)
1286         content->appendf("%s", m);   /* copy tail */
1287 
1288     content->terminate();
1289 
1290     assert((size_t)content->contentSize() == strlen(content->content()));
1291 
1292     return content;
1293 }
1294 
1295