/* * $Id: errorpage.cc,v 1.227 2007/05/29 13:31:39 amosjeffries Exp $ * * DEBUG: section 4 Error Generation * AUTHOR: Duane Wessels * * SQUID Web Proxy Cache http://www.squid-cache.org/ * ---------------------------------------------------------- * * Squid is the result of efforts by numerous individuals from * the Internet community; see the CONTRIBUTORS file for full * details. Many organizations have provided support for Squid's * development; see the SPONSORS file for full details. Squid is * Copyrighted (C) 2001 by the Regents of the University of * California; see the COPYRIGHT file for full details. Squid * incorporates software developed and/or copyrighted by other * sources; see the CREDITS file for full details. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA. * */ /* * Abstract: These routines are used to generate error messages to be * sent to clients. The error type is used to select between * the various message formats. (formats are stored in the * Config.errorDirectory) */ #include "errorpage.h" #include "AuthUserRequest.h" #include "SquidTime.h" #include "Store.h" #include "HttpReply.h" #include "HttpRequest.h" #include "MemObject.h" #include "fde.h" #include "MemBuf.h" #include "URLScheme.h" #include "wordlist.h" CBDATA_CLASS_INIT(ErrorState); /* local types */ typedef struct { int id; char *page_name; } ErrorDynamicPageInfo; /* local constant and vars */ /* * note: hard coded error messages are not appended with %S automagically * to give you more control on the format */ static const struct { int type; /* and page_id */ const char *text; } error_hard_text[] = { { ERR_SQUID_SIGNATURE, "\n
\n" "
\n" "
\n" "Generated %T by %h (%s)\n" "
\n" "\n" }, { TCP_RESET, "reset" } }; static Vector ErrorDynamicPages; /* local prototypes */ static const int error_hard_text_count = sizeof(error_hard_text) / sizeof(*error_hard_text); static char **error_text = NULL; static int error_page_count = 0; static char *errorTryLoadText(const char *page_name, const char *dir); static char *errorLoadText(const char *page_name); static const char *errorFindHardText(err_type type); static ErrorDynamicPageInfo *errorDynamicPageInfoCreate(int id, const char *page_name); static void errorDynamicPageInfoDestroy(ErrorDynamicPageInfo * info); static MemBuf *errorBuildContent(ErrorState * err); static int errorDump(ErrorState * err, MemBuf * mb); static const char *errorConvert(char token, ErrorState * err); static IOCB errorSendComplete; err_type &operator++ (err_type &anErr) { int tmp = (int)anErr; anErr = (err_type)(++tmp); return anErr; } int operator - (err_type const &anErr, err_type const &anErr2) { return (int)anErr - (int)anErr2; } /* * Function: errorInitialize * * Abstract: This function finds the error messages formats, and stores * them in error_text[]; * * Global effects: * error_text[] - is modified */ void errorInitialize(void) { err_type i; const char *text; error_page_count = ERR_MAX + ErrorDynamicPages.size(); error_text = static_cast(xcalloc(error_page_count, sizeof(char *))); for (i = ERR_NONE, ++i; i < error_page_count; ++i) { safe_free(error_text[i]); /* hard-coded ? */ if ((text = errorFindHardText(i))) error_text[i] = xstrdup(text); else if (i < ERR_MAX) { /* precompiled ? */ error_text[i] = errorLoadText(err_type_str[i]); } else { /* dynamic */ ErrorDynamicPageInfo *info = ErrorDynamicPages.items[i - ERR_MAX]; assert(info && info->id == i && info->page_name); if (strchr(info->page_name, ':') == NULL) { /* Not on redirected errors... */ error_text[i] = errorLoadText(info->page_name); } } } } void errorClean(void) { if (error_text) { int i; for (i = ERR_NONE + 1; i < error_page_count; i++) safe_free(error_text[i]); safe_free(error_text); } while (ErrorDynamicPages.size()) errorDynamicPageInfoDestroy(ErrorDynamicPages.pop_back()); error_page_count = 0; } static const char * errorFindHardText(err_type type) { int i; for (i = 0; i < error_hard_text_count; i++) if (error_hard_text[i].type == type) return error_hard_text[i].text; return NULL; } static char * errorLoadText(const char *page_name) { /* test configured location */ char *text = errorTryLoadText(page_name, Config.errorDirectory); /* test default location if failed */ if (!text && strcmp(Config.errorDirectory, DEFAULT_SQUID_ERROR_DIR)) text = errorTryLoadText(page_name, DEFAULT_SQUID_ERROR_DIR); /* giving up if failed */ if (!text) fatal("failed to find or read error text file."); return text; } static char * errorTryLoadText(const char *page_name, const char *dir) { int fd; char path[MAXPATHLEN]; char buf[4096]; char *text; ssize_t len; MemBuf textbuf; snprintf(path, sizeof(path), "%s/%s", dir, page_name); fd = file_open(path, O_RDONLY | O_TEXT); if (fd < 0) { debugs(4, 0, "errorTryLoadText: '" << path << "': " << xstrerror()); return NULL; } textbuf.init(); while((len = FD_READ_METHOD(fd, buf, sizeof(buf))) > 0) { textbuf.append(buf, len); } if (len < 0) { debugs(4, 0, "errorTryLoadText: failed to fully read: '" << path << "': " << xstrerror()); } file_close(fd); if (strstr(textbuf.buf, "%s") == NULL) textbuf.append("%S", 2); /* add signature */ /* Shrink memory size down to exact size. MemBuf has a tencendy * to be rather large.. */ text = xstrdup(textbuf.buf); textbuf.clean(); return text; } static ErrorDynamicPageInfo * errorDynamicPageInfoCreate(int id, const char *page_name) { ErrorDynamicPageInfo *info = new ErrorDynamicPageInfo; info->id = id; info->page_name = xstrdup(page_name); return info; } static void errorDynamicPageInfoDestroy(ErrorDynamicPageInfo * info) { assert(info); xfree(info->page_name); delete info; } static int errorPageId(const char *page_name) { for (int i = 0; i < ERR_MAX; i++) { if (strcmp(err_type_str[i], page_name) == 0) return i; } for (size_t j = 0; j < ErrorDynamicPages.size(); j++) { if (strcmp(ErrorDynamicPages.items[j]->page_name, page_name) == 0) return j + ERR_MAX; } return ERR_NONE; } err_type errorReservePageId(const char *page_name) { ErrorDynamicPageInfo *info; int id = errorPageId(page_name); if (id == ERR_NONE) { info = errorDynamicPageInfoCreate(ERR_MAX + ErrorDynamicPages.size(), page_name); ErrorDynamicPages.push_back(info); id = info->id; } return (err_type)id; } static const char * errorPageName(int pageId) { if (pageId >= ERR_NONE && pageId < ERR_MAX) /* common case */ return err_type_str[pageId]; if (pageId >= ERR_MAX && pageId - ERR_MAX < (ssize_t)ErrorDynamicPages.size()) return ErrorDynamicPages.items[pageId - ERR_MAX]->page_name; return "ERR_UNKNOWN"; /* should not happen */ } /* * Function: errorCon * * Abstract: This function creates a ErrorState object. */ ErrorState * errorCon(err_type type, http_status status, HttpRequest * request) { ErrorState *err = new ErrorState; err->page_id = type; /* has to be reset manually if needed */ err->type = type; err->httpStatus = status; if (request != NULL) { err->request = HTTPMSGLOCK(request); err->src_addr = request->client_addr; } return err; } /* * Function: errorAppendEntry * * Arguments: err - This object is destroyed after use in this function. * * Abstract: This function generates a error page from the info contained * by 'err' and then stores the text in the specified store * entry. This function should only be called by ``server * side routines'' which need to communicate errors to the * client side. It should also be called from client_side.c * because we now support persistent connections, and * cannot assume that we can immediately write to the socket * for an error. */ void errorAppendEntry(StoreEntry * entry, ErrorState * err) { HttpReply *rep; assert(entry->mem_obj != NULL); assert (entry->isEmpty()); debugs(4, 4, "Creating an error page for entry " << entry << " with errorstate " << err << " page id " << err->page_id); if (entry->store_status != STORE_PENDING) { debugs(4, 2, "Skipping error page due to store_status: " << entry->store_status); /* * If the entry is not STORE_PENDING, then no clients * care about it, and we don't need to generate an * error message */ assert(EBIT_TEST(entry->flags, ENTRY_ABORTED)); assert(entry->mem_obj->nclients == 0); errorStateFree(err); return; } if (err->page_id == TCP_RESET) { if (err->request) { debugs(4, 2, "RSTing this reply"); err->request->flags.setResetTCP(); } } entry->lock(); entry->buffer(); rep = errorBuildReply(err); /* Add authentication header */ /* TODO: alter errorstate to be accel on|off aware. The 0 on the next line * depends on authenticate behaviour: all schemes to date send no extra * data on 407/401 responses, and do not check the accel state on 401/407 * responses */ authenticateFixHeader(rep, err->auth_user_request, err->request, 0, 1); entry->replaceHttpReply(rep); EBIT_CLR(entry->flags, ENTRY_FWD_HDR_WAIT); entry->flush(); entry->complete(); entry->negativeCache(); entry->releaseRequest(); entry->unlock(); errorStateFree(err); } /* * Function: errorSend * * Arguments: err - This object is destroyed after use in this function. * * Abstract: This function generates a error page from the info contained * by 'err' and then sends it to the client. * The callback function errorSendComplete() is called after * the page has been written to the client socket (fd). * errorSendComplete() deallocates 'err'. We need to add * 'err' to the cbdata because comm_write() requires it * for all callback data pointers. * * Note, normally errorSend() should only be called from * routines in ssl.c and pass.c, where we don't have any * StoreEntry's. In client_side.c we must allocate a StoreEntry * for errors and use errorAppendEntry() to account for * persistent/pipeline connections. */ void errorSend(int fd, ErrorState * err) { HttpReply *rep; debugs(4, 3, "errorSend: FD " << fd << ", err=" << err); assert(fd >= 0); /* * ugh, this is how we make sure error codes get back to * the client side for logging and error tracking. */ if (err->request) err->request->errType = err->type; /* moved in front of errorBuildBuf @?@ */ err->flags.flag_cbdata = 1; rep = errorBuildReply(err); comm_write_mbuf(fd, rep->pack(), errorSendComplete, err); delete rep; } /* * Function: errorSendComplete * * Abstract: Called by commHandleWrite() after data has been written * to the client socket. * * Note: If there is a callback, the callback is responsible for * closing the FD, otherwise we do it ourseves. */ static void errorSendComplete(int fd, char *bufnotused, size_t size, comm_err_t errflag, int xerrno, void *data) { ErrorState *err = static_cast(data); debugs(4, 3, "errorSendComplete: FD " << fd << ", size=" << size); if (errflag != COMM_ERR_CLOSING) { if (err->callback) { debugs(4, 3, "errorSendComplete: callback"); err->callback(fd, err->callback_data, size); } else { comm_close(fd); debugs(4, 3, "errorSendComplete: comm_close"); } } errorStateFree(err); } void errorStateFree(ErrorState * err) { HTTPMSGUNLOCK(err->request); safe_free(err->redirect_url); safe_free(err->url); safe_free(err->dnsserver_msg); safe_free(err->request_hdrs); wordlistDestroy(&err->ftp.server_msg); safe_free(err->ftp.request); safe_free(err->ftp.reply); AUTHUSERREQUESTUNLOCK(err->auth_user_request, "errstate"); safe_free(err->err_msg); cbdataFree(err); } static int errorDump(ErrorState * err, MemBuf * mb) { HttpRequest *r = err->request; MemBuf str; const char *p = NULL; /* takes priority over mb if set */ str.reset(); /* email subject line */ str.Printf("CacheErrorInfo - %s", errorPageName(err->type)); mb->Printf("?subject=%s", rfc1738_escape_part(str.buf)); str.reset(); /* email body */ str.Printf("CacheHost: %s\r\n", getMyHostname()); /* - Err Msgs */ str.Printf("ErrPage: %s\r\n", errorPageName(err->type)); if (err->xerrno) { str.Printf("Err: (%d) %s\r\n", err->xerrno, strerror(err->xerrno)); } else { str.Printf("Err: [none]\r\n"); } if (err->auth_user_request->denyMessage()) str.Printf("Auth ErrMsg: %s\r\n", err->auth_user_request->denyMessage()); if (err->dnsserver_msg) { str.Printf("DNS Server ErrMsg: %s\r\n", err->dnsserver_msg); } /* - TimeStamp */ str.Printf("TimeStamp: %s\r\n\r\n", mkrfc1123(squid_curtime)); /* - IP stuff */ str.Printf("ClientIP: %s\r\n", inet_ntoa(err->src_addr)); if (r && r->hier.host[0] != '\0') { str.Printf("ServerIP: %s\r\n", r->hier.host); } str.Printf("\r\n"); /* - HTTP stuff */ str.Printf("HTTP Request:\r\n"); if (NULL != r) { Packer p; str.Printf("%s %s HTTP/%d.%d\n", RequestMethodStr[r->method], r->urlpath.size() ? r->urlpath.buf() : "/", r->http_ver.major, r->http_ver.minor); packerToMemInit(&p, &str); r->header.packInto(&p); packerClean(&p); } else if (err->request_hdrs) { p = err->request_hdrs; } else { p = "[none]"; } str.Printf("\r\n"); /* - FTP stuff */ if (err->ftp.request) { str.Printf("FTP Request: %s\r\n", err->ftp.request); str.Printf("FTP Reply: %s\r\n", err->ftp.reply); str.Printf("FTP Msg: "); wordlistCat(err->ftp.server_msg, &str); str.Printf("\r\n"); } str.Printf("\r\n"); mb->Printf("&body=%s", rfc1738_escape_part(str.buf)); str.clean(); return 0; } #define CVT_BUF_SZ 512 /* * a - User identity x * B - URL with FTP %2f hack x * c - Squid error code x * d - seconds elapsed since request received x * e - errno x * E - strerror() x * f - FTP request line x * F - FTP reply line x * g - FTP server message x * h - cache hostname x * H - server host name x * i - client IP address x * I - server IP address x * L - HREF link for more info/contact x * M - Request Method x * m - Error message returned by auth helper x * o - Message returned external acl helper x * p - URL port # x * P - Protocol x * R - Full HTTP Request x * S - squid signature from ERR_SIGNATURE x * s - caching proxy software with version x * t - local time x * T - UTC x * U - URL without password x * u - URL with password x * w - cachemgr email address x * W - error data (to be included in the mailto links) * z - dns server error message x * Z - Preformatted error message x */ static const char * errorConvert(char token, ErrorState * err) { HttpRequest *r = err->request; static MemBuf mb; const char *p = NULL; /* takes priority over mb if set */ int do_quote = 1; mb.reset(); switch (token) { case 'a': if (r && r->auth_user_request) p = r->auth_user_request->username(); if (!p) p = "-"; break; case 'B': p = r ? ftpUrlWith2f(r) : "[no URL]"; break; case 'c': p = errorPageName(err->type); break; case 'e': mb.Printf("%d", err->xerrno); break; case 'E': if (err->xerrno) mb.Printf("(%d) %s", err->xerrno, strerror(err->xerrno)); else mb.Printf("[No Error]"); break; case 'f': /* FTP REQUEST LINE */ if (err->ftp.request) p = err->ftp.request; else p = "nothing"; break; case 'F': /* FTP REPLY LINE */ if (err->ftp.request) p = err->ftp.reply; else p = "nothing"; break; case 'g': /* FTP SERVER MESSAGE */ wordlistCat(err->ftp.server_msg, &mb); break; case 'h': mb.Printf("%s", getMyHostname()); break; case 'H': if (r) { if (r->hier.host[0] != '\0') // if non-empty string. p = r->hier.host; else p = r->host; } else p = "[unknown host]"; break; case 'i': mb.Printf("%s", inet_ntoa(err->src_addr)); break; case 'I': if (r && r->hier.host[0] != '\0') // if non-empty string mb.Printf("%s", r->hier.host); else p = "[unknown]"; break; case 'L': if (Config.errHtmlText) { mb.Printf("%s", Config.errHtmlText); do_quote = 0; } else p = "[not available]"; break; case 'm': p = err->auth_user_request->denyMessage("[not available]"); break; case 'M': p = r ? RequestMethodStr[r->method] : "[unknown method]"; break; case 'o': p = external_acl_message ? external_acl_message : "[not available]"; break; case 'p': if (r) { mb.Printf("%d", (int) r->port); } else { p = "[unknown port]"; } break; case 'P': p = r ? ProtocolStr[r->protocol] : "[unknown protocol]"; break; case 'R': if (NULL != r) { Packer p; mb.Printf("%s %s HTTP/%d.%d\n", RequestMethodStr[r->method], r->urlpath.size() ? r->urlpath.buf() : "/", r->http_ver.major, r->http_ver.minor); packerToMemInit(&p, &mb); r->header.packInto(&p); packerClean(&p); } else if (err->request_hdrs) { p = err->request_hdrs; } else { p = "[no request]"; } break; case 's': p = visible_appname_string; break; case 'S': /* signature may contain %-escapes, recursion */ if (err->page_id != ERR_SQUID_SIGNATURE) { const int saved_id = err->page_id; err->page_id = ERR_SQUID_SIGNATURE; MemBuf *sign_mb = errorBuildContent(err); mb.Printf("%s", sign_mb->content()); sign_mb->clean(); delete sign_mb; err->page_id = saved_id; do_quote = 0; } else { /* wow, somebody put %S into ERR_SIGNATURE, stop recursion */ p = "[%S]"; } break; case 't': mb.Printf("%s", mkhttpdlogtime(&squid_curtime)); break; case 'T': mb.Printf("%s", mkrfc1123(squid_curtime)); break; case 'U': /* Using the fake-https version of canonical so error pages see https:// */ /* even when the url-path cannot be shown as more than '*' */ p = r ? urlCanonicalFakeHttps(r) : err->url ? err->url : "[no URL]"; break; case 'u': p = r ? urlCanonical(r) : err->url ? err->url : "[no URL]"; break; case 'w': if (Config.adminEmail) mb.Printf("%s", Config.adminEmail); else p = "[unknown]"; break; case 'W': if (Config.adminEmail && Config.onoff.emailErrData) errorDump(err, &mb); break; case 'z': if (err->dnsserver_msg) p = err->dnsserver_msg; else p = "[unknown]"; break; case 'Z': if (err->err_msg) p = err->err_msg; else p = "[unknown]"; break; case '%': p = "%"; break; default: mb.Printf("%%%c", token); do_quote = 0; break; } if (!p) p = mb.buf; /* do not use mb after this assignment! */ assert(p); debugs(4, 3, "errorConvert: %%" << token << " --> '" << p << "'" ); if (do_quote) p = html_quote(p); return p; } /* allocates and initializes an error response */ HttpReply * errorBuildReply(ErrorState * err) { HttpReply *rep = new HttpReply; const char *name = errorPageName(err->page_id); /* no LMT for error pages; error pages expire immediately */ HttpVersion version(1, 0); if (strchr(name, ':')) { /* Redirection */ rep->setHeaders(version, HTTP_MOVED_TEMPORARILY, NULL, "text/html", 0, 0, -1); if (err->request) { char *quoted_url = rfc1738_escape_part(urlCanonical(err->request)); httpHeaderPutStrf(&rep->header, HDR_LOCATION, name, quoted_url); } httpHeaderPutStrf(&rep->header, HDR_X_SQUID_ERROR, "%d %s", err->httpStatus, "Access Denied"); } else { MemBuf *content = errorBuildContent(err); rep->setHeaders(version, err->httpStatus, NULL, "text/html", content->contentSize(), 0, -1); /* * include some information for downstream caches. Implicit * replaceable content. This isn't quite sufficient. xerrno is not * necessarily meaningful to another system, so we really should * expand it. Additionally, we should identify ourselves. Someone * might want to know. Someone _will_ want to know OTOH, the first * X-CACHE-MISS entry should tell us who. */ httpHeaderPutStrf(&rep->header, HDR_X_SQUID_ERROR, "%s %d", name, err->xerrno); httpBodySet(&rep->body, content); /* do not memBufClean() or delete the content, it was absorbed by httpBody */ } return rep; } static MemBuf * errorBuildContent(ErrorState * err) { MemBuf *content = new MemBuf; const char *m; const char *p; const char *t; assert(err != NULL); assert(err->page_id > ERR_NONE && err->page_id < error_page_count); content->init(); m = error_text[err->page_id]; assert(m); while ((p = strchr(m, '%'))) { content->append(m, p - m); /* copy */ t = errorConvert(*++p, err); /* convert */ content->Printf("%s", t); /* copy */ m = p + 1; /* advance */ } if (*m) content->Printf("%s", m); /* copy tail */ assert((size_t)content->contentSize() == strlen(content->content())); return content; }