From: Christos Tsantilas Date: Fri, 17 Jun 2011 07:46:48 +0000 (+0300) Subject: Configurable SSL error details messages X-Git-Tag: take08~55^2~127 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=02259ff;p=thirdparty%2Fsquid.git Configurable SSL error details messages This project adds support for a translatable and customisable error detail file (errors/templates/error_details.txt). The file is stored like we store error page templates today. Inside the file, an HTTP-like format used that can be later extended to other error details (and beyond): name: value details: "value" descr: "value" or name: value details: "multi line value" descr: "value with a \"quoted string\" inside" The code supports future translations, just like Squid already support error page translations. This is a Measurement Factory project ---- Some Technical details: - The errorpage code which is related to loading and parsing error templates moved to TemplateFile class. This class is used as base class for ErrorPageFile class which used to load error page templates. - The HttpHeader parser used to parse error details - The error details for various languages cached to memory - The ErrorDetailsList used to store a list of error details for a language/locale - The ErrorDetailsManager is a class used to load and manage multiple error details list (ErrorDetailsList objects) for many languages. It also implements a simple cache. --- diff --git a/configure.ac b/configure.ac index 21b0603fdc..462aeb09cf 100644 --- a/configure.ac +++ b/configure.ac @@ -3323,10 +3323,13 @@ SQUID_YESNO([$enableval], dnl Squid now has .po translation capability, given the right toolkit if test "x${enable_translation:=yes}" = "xyes" ; then AX_WITH_PROG([PO2HTML],[po2html]) + AX_WITH_PROG([PO2TEXT],[po2txt]) else PO2HTML="off" + PO2TEXT="off" fi AC_SUBST(PO2HTML) +AC_SUBST(PO2TEXT) dnl Squid now has limited locale handling ... dnl on error pages diff --git a/errors/Makefile.am b/errors/Makefile.am index 776ae2dd02..a8fdc90303 100644 --- a/errors/Makefile.am +++ b/errors/Makefile.am @@ -17,6 +17,7 @@ include $(srcdir)/language.list CLEANFILES = $(TRANSLATE_LANGUAGES) translate-warn EXTRA_DIST = \ $(ERROR_TEMPLATES) \ + templates/error-details.txt \ language.list \ template.list \ aliases alias-link.sh alias-upgrade errorpage.css TRANSLATORS COPYRIGHT @@ -46,10 +47,11 @@ NOTIDY=`$(PO2HTML) --help | grep -o "\-\-notidy"` mkdir -p $(top_builddir)/errors/$$lang; \ echo -n "Translate '$$lang' ..."; \ for f in $(ERROR_TEMPLATES); do \ - page=`basename $$f`; \ - $(PO2HTML) $(NOTIDY) --progress=none -i $(top_srcdir)/errors/$$lang.po -t $(top_srcdir)/errors/$$f >$(top_builddir)/errors/$$lang/$$page || exit 1; \ - done; \ - echo "done."; \ + page=`basename $$f`; \ + $(PO2HTML) $(NOTIDY) --progress=none -i $(top_srcdir)/errors/$$lang.po -t $(top_srcdir)/errors/$$f >$(top_builddir)/errors/$$lang/$$page || exit 1; \ + done; \ + $(PO2TEXT) -t $(top_srcdir)/errors/templates/error-details.txt -i $(top_srcdir)/errors/$$lang.po > $(top_builddir)/errors/$$lang/error-details.txt || exit 1; \ + echo "done."; \ fi; \ touch $@ @@ -70,7 +72,7 @@ install-data-local: translate if test -d $(srcdir)/$$l || test -d $(builddir)/$$l; then \ $(mkinstalldirs) $(DESTDIR)$(DEFAULT_ERROR_DIR)/$$l; \ fi; \ - for f in $(ERROR_TEMPLATES); do \ + for f in $(ERROR_TEMPLATES) templates/error-details.txt; do \ page=`basename $$f`; \ if test -f $(builddir)/$$l/$$page; then \ echo "$(INSTALL_DATA) $(builddir)/$$l/$$page $(DESTDIR)$(DEFAULT_ERROR_DIR)/$$l"; \ @@ -91,14 +93,14 @@ uninstall-local: l=`basename $$l .lang`; \ echo "Located $$l for uninstall ..."; \ if test -d $(srcdir)/$$l; then \ - for f in $(srcdir)/$$l/ERR_*; do \ + for f in $(srcdir)/$$l/ERR_* $(srcdir)/$$l/error-details.txt; do \ if test -f $(DESTDIR)$(DEFAULT_ERROR_DIR)/$$l/`basename $$f`; then \ $(RM) $(DESTDIR)$(DEFAULT_ERROR_DIR)/$$l/`basename $$f`; \ fi; \ done; \ fi ; \ if test -d $(builddir)/$$l; then \ - for f in $(builddir)/$$l/ERR_*; do \ + for f in $(builddir)/$$l/ERR_* $(builddir)/$$l/error-details.txt; do \ if test -f $(DESTDIR)$(DEFAULT_ERROR_DIR)/$$l/`basename $$f`; then \ $(RM) $(DESTDIR)$(DEFAULT_ERROR_DIR)/$$l/`basename $$f`; \ fi; \ @@ -119,8 +121,8 @@ dist-hook: translate lang=`basename $$lang .lang`; \ if test -d $$lang ; then \ mkdir -p $(distdir)/$$lang; \ - cp -p $(top_builddir)/errors/$$lang/ERR_* $(distdir)/$$lang \ - || exit 1; \ + cp -p $(top_builddir)/errors/$$lang/ERR_* $(distdir)/$$lang || exit 1; \ + cp -p $(top_builddir)/errors/$$lang/error-details.txt $(distdir)/$$lang || exit 1; \ fi; \ done diff --git a/errors/templates/error-details.txt b/errors/templates/error-details.txt new file mode 100644 index 0000000000..af0152b971 --- /dev/null +++ b/errors/templates/error-details.txt @@ -0,0 +1,127 @@ +name: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT +detail: "SSL Certficate error: certificate issuer (CA) not known: %ssl_ca_name" +descr: "Unable to get issuer certificate" + +name: X509_V_ERR_UNABLE_TO_GET_CRL +detail: "%ssl_error_descr: %ssl_subject" +descr: "Unable to get certificate CRL" + +name: X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE +detail: "%ssl_error_descr: %ssl_subject" +descr: "Unable to decrypt certificate's signature" + +name: X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE +detail: "%ssl_error_descr: %ssl_subject" +descr: "Unable to decrypt CRL's signature" + +name: X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY +detail: "Unable to decode issuer (CA) public key: %ssl_ca_name" +descr: "Unable to decode issuer public key" + +name: X509_V_ERR_CERT_SIGNATURE_FAILURE +detail: "%ssl_error_descr: %ssl_subject" +descr: "Certificate signature failure" + +name: X509_V_ERR_CRL_SIGNATURE_FAILURE +detail: "%ssl_error_descr: %ssl_subject" +descr: "CRL signature failure" + +name: X509_V_ERR_CERT_NOT_YET_VALID +detail: "SSL Certficate is not valid before: %ssl_notbefore" +descr: "Certificate is not yet valid" + +name: X509_V_ERR_CERT_HAS_EXPIRED +detail: "SSL Certificate expired on: %ssl_notafter" +descr: "Certificate has expired" + +name: X509_V_ERR_CRL_NOT_YET_VALID +detail: "%ssl_error_descr: %ssl_subject" +descr: "CRL is not yet valid" + +name: X509_V_ERR_CRL_HAS_EXPIRED +detail: "%ssl_error_descr: %ssl_subject" +descr: "CRL has expired" + +name: X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD +detail: "SSL Certificate has invalid start date (the 'not before' field): %ssl_subject" +descr: "Format error in certificate's notBefore field" + +name: X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD +detail: "SSL Certificate has invalid expiration date (the 'not after' field): %ssl_subject" +descr: "Format error in certificate's notAfter field" + +name: X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD +detail: "%ssl_error_descr: %ssl_subject" +descr: "Format error in CRL's lastUpdate field" + +name: X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD +detail: "%ssl_error_descr: %ssl_subject" +descr: "Format error in CRL's nextUpdate field" + +name: X509_V_ERR_OUT_OF_MEM +detail: "%ssl_error_descr" +descr: "Out of memory" + +name: X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT +detail: "Self-signed SSL Certificate: %ssl_subject" +descr: "Self signed certificate" + +name: X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN +detail: "Self-signed SSL Certificate in chain: %ssl_subject" +descr: "Self signed certificate in certificate chain" + +name: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY +detail: "SSL Certficate error: certificate issuer (CA) not known: %ssl_ca_name" +descr: "Unable to get local issuer certificate" + +name: X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE +detail: "%ssl_error_descr: %ssl_subject" +descr: "Unable to verify the first certificate" + +name: X509_V_ERR_CERT_CHAIN_TOO_LONG +detail: "%ssl_error_descr: %ssl_subject" +descr: "Certificate chain too long" + +name: X509_V_ERR_CERT_REVOKED +detail: "%ssl_error_descr: %ssl_subject" +descr: "Certificate revoked" + +name: X509_V_ERR_INVALID_CA +detail: "%ssl_error_descr: %ssl_ca_name" +descr: "Invalid CA certificate" + +name: X509_V_ERR_PATH_LENGTH_EXCEEDED +detail: "%ssl_error_descr: %ssl_subject" +descr: "Path length constraint exceeded" + +name: X509_V_ERR_INVALID_PURPOSE +detail: "%ssl_error_descr: %ssl_subject" +descr: "Unsupported certificate purpose" + +name: X509_V_ERR_CERT_UNTRUSTED +detail: "%ssl_error_descr: %ssl_subject" +descr: "Certificate not trusted" + +name: X509_V_ERR_CERT_REJECTED +detail: "%ssl_error_descr: %ssl_subject" +descr: "Certificate rejected" + +name: X509_V_ERR_SUBJECT_ISSUER_MISMATCH +detail: "%ssl_error_descr: %ssl_ca_name" +descr: "Subject issuer mismatch" + +name: X509_V_ERR_AKID_SKID_MISMATCH +detail: "%ssl_error_descr: %ssl_subject" +descr: "Authority and subject key identifier mismatch" + +name: X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH +detail: "%ssl_error_descr: %ssl_ca_name" +descr: "Authority and issuer serial number mismatch" + +name: X509_V_ERR_KEYUSAGE_NO_CERTSIGN +detail: "%ssl_error_descr: %ssl_subject" +descr: "Key usage does not include certificate signing" + +name: X509_V_ERR_APPLICATION_VERIFICATION +detail: "%ssl_error_descr: %ssl_subject" +descr: "Application verification failure" diff --git a/scripts/mk-error-details-po.pl b/scripts/mk-error-details-po.pl new file mode 100644 index 0000000000..1767467115 --- /dev/null +++ b/scripts/mk-error-details-po.pl @@ -0,0 +1,137 @@ +#!/usr/bin/perl -w +# +# Author: Tsantilas Christos +# (C) 2011 The Measurement Factory +# +# Usage: +# mk-error-details-po.pl error-details.txt +# +# This script read the error-details.txt error details template, and prints to the +# std output the contents of a possible .po file. +# The error-details.txt file consist of records like the following: +# +# name: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT +# detail: "SSL Certficate error: certificate issuer (CA) not known: %ssl_ca_name" +# descr: "Unable to get issuer certificate" +# +# The records separated with an empty line. +# Comments starting with '#' supported. +# + +use warnings; +use strict; + +my $File; +my $mode; + +$File = shift @ARGV or + die "Usage: \n ".$0." error-detail-file\n\n"; + +open(IN, "<$File") or + die "Can not open file '$File': $!"; + +my @PO_RECORDS = (); +my $lineNumber=0; +while(my $line = ) { + $lineNumber++; + + if ($line =~ /^\#.*/ ) { + next; + } + elsif ($line =~ /^\s*$/ ) { + next; + } + my($rec) = ""; + my($lineOffset) = 0; + do { + $rec = $rec.$line; + $line = ; + $lineOffset++; + } while($line && $line !~ /^\s*$/); + + processRecord(\@PO_RECORDS, $rec, $lineNumber); + $lineNumber= $lineNumber + $lineOffset; +} + +foreach my $poRec (@PO_RECORDS) { + print $poRec->{"comment"}; + print "msgid ".$poRec->{"msgid"}."\n"; + print "msgstr ".$poRec->{"msgstr"}."\n\n"; +} + +exit(0); + + +sub processRecord +{ + my($RECS, $rec, $lnumber) = @_; + my(@lines) = split /\n/, $rec; + my($currentField) = ""; + my(%currentRec); + my $offset = 0; + foreach my $l (@lines) { + if ($l =~ /^name:(.*)/) { + $currentRec{"name"} = trim($1); + $currentField = "name"; + } + elsif ( $l =~ /^detail:(.*)/ ) { + $currentRec{"detail"} = toCstr(trim_quoted($1)); + $currentField = "detail"; + } + elsif ($l =~ /^descr:(.*)/) { + $currentRec{"descr"} = toCstr(trim_quoted($1)); + $currentField = "descr"; + } + elsif($l = ~ /^(\s+.*)/ && defined($currentRec{$currentField})) { + my($fmtl) = toCstr($1); + $currentRec{$currentField}= $currentRec{$currentField}."\\n".$fmtl; + } + } + + my (%poRecDetail, %poRecDescr); + + $poRecDetail{"comment"} = "#: $File+".$currentRec{"name"}.".detail:$lnumber\n"; + $poRecDetail{"msgid"} = $currentRec{"detail"}; + $poRecDetail{"msgstr"} = $currentRec{"detail"}; + merge(\@$RECS, \%poRecDetail); + + $poRecDescr{"comment"} = "#: $File+".$currentRec{"name"}.".descr:$lnumber\n"; + $poRecDescr{"msgid"} = $currentRec{"descr"}; + $poRecDescr{"msgstr"} = $currentRec{"descr"}; + merge(\@$RECS, \%poRecDescr); +} + +sub merge { + my ($RECS, $poRec) = @_; + foreach my $item (@$RECS) { + if ($poRec->{"msgid"} eq $item->{"msgid"}) { + $item->{"comment"} = $item->{"comment"}.$poRec->{"comment"}; + return; + } + } + push @$RECS, $poRec; + return; +} + +sub trim +{ + my $string = shift; + $string =~ s/^\s+//; + $string =~ s/\s+$//; + return $string; +} + +sub trim_quoted +{ + my $string = shift; + $string =~ s/^\s+"/"/; + $string =~ s/"\s+$/"/; + return $string; +} + +sub toCstr +{ + my $string = shift; + $string =~ s/\t/\\t/g; + return $string; +} diff --git a/scripts/update-pot.sh b/scripts/update-pot.sh index 9d9cdba772..158ba2aa70 100755 --- a/scripts/update-pot.sh +++ b/scripts/update-pot.sh @@ -17,11 +17,18 @@ rm errpages.pot # make a temp directory for all our workings... mkdir pot -# Generate per-page disctionaries ... +# Generate per-page dictionaries ... for f in `ls -1 ./templates/`; do - if test "${f}" != "generic" ; then + case ${f} in + error-details.txt) + ../scripts/mk-error-details-po.pl ./templates/${f} > ./pot/${f}.pot + ;; + ERR_*) html2po -i ./templates/${f} -P --duplicates=merge -o ./pot/${f}.pot - fi + ;; + *) + echo "SKIP: ${f}" + esac done # merge and sort the per-page .pot into a single master diff --git a/src/HttpHeader.cc b/src/HttpHeader.cc index 30ef238958..10bd05bb85 100644 --- a/src/HttpHeader.cc +++ b/src/HttpHeader.cc @@ -385,7 +385,7 @@ HttpHeader::HttpHeader() : owner (hoNone), len (0) HttpHeader::HttpHeader(const http_hdr_owner_type anOwner): owner(anOwner), len(0) { - assert(anOwner > hoNone && anOwner <= hoReply); + assert(anOwner > hoNone && anOwner < hoEnd); debugs(55, 7, "init-ing hdr: " << this << " owner: " << owner); httpHeaderMaskInit(&mask, 0); } @@ -420,7 +420,7 @@ HttpHeader::clean() HttpHeaderPos pos = HttpHeaderInitPos; HttpHeaderEntry *e; - assert(owner > hoNone && owner <= hoReply); + assert(owner > hoNone && owner < hoEnd); debugs(55, 7, "cleaning hdr: " << this << " owner: " << owner); PROF_start(HttpHeaderClean); @@ -436,24 +436,26 @@ HttpHeader::clean() * arrays. */ - if (0 != entries.count) - statHistCount(&HttpHeaderStats[owner].hdrUCountDistr, entries.count); + if (owner <= hoReply) { + if (0 != entries.count) + statHistCount(&HttpHeaderStats[owner].hdrUCountDistr, entries.count); - HttpHeaderStats[owner].destroyedCount++; + HttpHeaderStats[owner].destroyedCount++; - HttpHeaderStats[owner].busyDestroyedCount += entries.count > 0; + HttpHeaderStats[owner].busyDestroyedCount += entries.count > 0; - while ((e = getEntry(&pos))) { - /* tmp hack to try to avoid coredumps */ + while ((e = getEntry(&pos))) { + /* tmp hack to try to avoid coredumps */ - if (e->id < 0 || e->id >= HDR_ENUM_END) { - debugs(55, 0, "HttpHeader::clean BUG: entry[" << pos << "] is invalid (" << e->id << "). Ignored."); - } else { - statHistCount(&HttpHeaderStats[owner].fieldTypeDistr, e->id); - /* yes, this deletion leaves us in an inconsistent state */ - delete e; + if (e->id < 0 || e->id >= HDR_ENUM_END) { + debugs(55, 0, "HttpHeader::clean BUG: entry[" << pos << "] is invalid (" << e->id << "). Ignored."); + } else { + statHistCount(&HttpHeaderStats[owner].fieldTypeDistr, e->id); + /* yes, this deletion leaves us in an inconsistent state */ + delete e; + } } - } + } // if (owner <= hoReply) entries.clean(); httpHeaderMaskInit(&mask, 0); len = 0; diff --git a/src/HttpHeader.h b/src/HttpHeader.h index a9cf983be1..84fa95b12e 100644 --- a/src/HttpHeader.h +++ b/src/HttpHeader.h @@ -157,7 +157,11 @@ typedef enum { hoHtcpReply, #endif hoRequest, - hoReply + hoReply, +#if USE_SSL + hoErrorDetail, +#endif + hoEnd } http_hdr_owner_type; struct _HttpHeaderFieldAttrs { diff --git a/src/errorpage.cc b/src/errorpage.cc index db7ecc342b..1f380cedff 100644 --- a/src/errorpage.cc +++ b/src/errorpage.cc @@ -128,13 +128,30 @@ static int error_page_count = 0; /// \ingroup ErrorPageInternal static MemBuf error_stylesheet; -static char *errorTryLoadText(const char *page_name, const char *dir, bool silent = false); -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 IOCB errorSendComplete; +/// \ingroup ErrorPageInternal +/// manages an error page template +class ErrorPageFile: public TemplateFile { +public: + ErrorPageFile(const char *name): TemplateFile(name) { textBuf.init();} + + /// The template text data read from disk + const char *text() { return textBuf.content(); } + +private: + /// stores the data read from disk to a local buffer + virtual bool parse(const char *buf, int len, bool eof) { + if (len) + textBuf.append(buf, len); + return true; + } + + MemBuf textBuf; ///< A buffer to store the error page +}; /// \ingroup ErrorPageInternal err_type &operator++ (err_type &anErr) @@ -173,8 +190,8 @@ errorInitialize(void) * (a) default language translation directory (error_default_language) * (b) admin specified custom directory (error_directory) */ - error_text[i] = errorLoadText(err_type_str[i]); - + ErrorPageFile errTmpl(err_type_str[i]); + error_text[i] = errTmpl.loadDefault() ? xstrdup(errTmpl.text()) : NULL; } else { /** \par * Index any unknown file names used by deny_info. @@ -188,7 +205,8 @@ errorInitialize(void) if (strchr(pg, ':') == NULL) { /** But only if they are not redirection URL. */ - error_text[i] = errorLoadText(pg); + ErrorPageFile errTmpl(pg); + error_text[i] = errTmpl.loadDefault() ? xstrdup(errTmpl.text()) : NULL; } } } @@ -197,12 +215,12 @@ errorInitialize(void) // look for and load stylesheet into global MemBuf for it. if (Config.errorStylesheet) { - char *temp = errorTryLoadText(Config.errorStylesheet,NULL); - if (temp) { - error_stylesheet.Printf("%s",temp); - safe_free(temp); - } + ErrorPageFile tmpl("StylesSheet"); + tmpl.loadFromFile(Config.errorStylesheet); + error_stylesheet.Printf("%s",tmpl.text()); } + + Ssl::errorDetailInitialize(); } void @@ -221,6 +239,8 @@ errorClean(void) errorDynamicPageInfoDestroy(ErrorDynamicPages.pop_back()); error_page_count = 0; + + Ssl::errorDetailClean(); } /// \ingroup ErrorPageInternal @@ -236,63 +256,79 @@ errorFindHardText(err_type type) return NULL; } -/** - * \ingroup ErrorPageInternal - * - * Load into the in-memory error text Index a file probably available at: - * (a) admin specified custom directory (error_directory) - * (b) default language translation directory (error_default_language) - * (c) English sub-directory where errors should ALWAYS exist - */ -static char * -errorLoadText(const char *page_name) +TemplateFile::TemplateFile(const char *name): silent(false), wasLoaded(false), templateName(name) { - char *text = NULL; + assert(name); +} + +bool +TemplateFile::loadDefault() +{ + if (loaded()) // already loaded? + return true; /** test error_directory configured location */ - if (Config.errorDirectory) - text = errorTryLoadText(page_name, Config.errorDirectory); + if (Config.errorDirectory) { + char path[MAXPATHLEN]; + snprintf(path, sizeof(path), "%s/%s", Config.errorDirectory, templateName.termedBuf()); + loadFromFile(path); + } #if USE_ERR_LOCALES /** test error_default_language location */ - if (!text && Config.errorDefaultLanguage) { - char dir[256]; - snprintf(dir,256,"%s/%s", DEFAULT_SQUID_ERROR_DIR, Config.errorDefaultLanguage); - text = errorTryLoadText(page_name, dir); - if (!text) { + if (!loaded() && Config.errorDefaultLanguage) { + if (!tryLoadTemplate(Config.errorDefaultLanguage)) { debugs(1, DBG_CRITICAL, "Unable to load default error language files. Reset to backups."); } } #endif /* test default location if failed (templates == English translation base templates) */ - if (!text) { - text = errorTryLoadText(page_name, DEFAULT_SQUID_ERROR_DIR"/templates"); + if (!loaded()) { + tryLoadTemplate("templates"); } /* giving up if failed */ - if (!text) + if (!loaded()) fatal("failed to find or read error text file."); - return text; + return true; } -/// \ingroup ErrorPageInternal -static char * -errorTryLoadText(const char *page_name, const char *dir, bool silent) +bool +TemplateFile::tryLoadTemplate(const char *lang) { - int fd; + assert(lang); + char path[MAXPATHLEN]; + /* TODO: prep the directory path string to prevent snprintf ... */ + snprintf(path, sizeof(path), "%s/%s/%s", + DEFAULT_SQUID_ERROR_DIR, lang, templateName.termedBuf()); + path[MAXPATHLEN-1] = '\0'; + + if (loadFromFile(path)) + return true; + +#if HAVE_GLOB + if ( strlen(lang) == 2) { + /* TODO glob the error directory for sub-dirs matching: '-*' */ + /* use first result. */ + debugs(4,2, HERE << "wildcard fallback errors not coded yet."); + } +#endif + + return false; +} + +bool +TemplateFile::loadFromFile(const char *path) +{ + int fd; char buf[4096]; - char *text; ssize_t len; - MemBuf textbuf; - // maybe received compound parts, maybe an absolute page_name and no dir - if (dir) - snprintf(path, sizeof(path), "%s/%s", dir, page_name); - else - snprintf(path, sizeof(path), "%s", page_name); + if (loaded()) // already loaded? + return true; fd = file_open(path, O_RDONLY | O_TEXT); @@ -300,14 +336,18 @@ errorTryLoadText(const char *page_name, const char *dir, bool silent) /* with dynamic locale negotiation we may see some failures before a success. */ if (!silent) debugs(4, DBG_CRITICAL, HERE << "'" << path << "': " << xstrerror()); - return NULL; + wasLoaded = false; + return wasLoaded; } - textbuf.init(); - while ((len = FD_READ_METHOD(fd, buf, sizeof(buf))) > 0) { - textbuf.append(buf, len); + if (!parse(buf, len, false)) { + debugs(4, DBG_CRITICAL, HERE << " parse error while reading template file: " << path); + wasLoaded = false; + return wasLoaded; + } } + parse(buf, 0, true); if (len < 0) { debugs(4, DBG_CRITICAL, HERE << "failed to fully read: '" << path << "': " << xstrerror()); @@ -315,14 +355,98 @@ errorTryLoadText(const char *page_name, const char *dir, bool silent) file_close(fd); - /* Shrink memory size down to exact size. MemBuf has a tencendy - * to be rather large.. - */ - text = xstrdup(textbuf.buf); + wasLoaded = true; + return wasLoaded; +} + +bool strHdrAcptLangGetItem(const String &hdr, char *lang, int langLen, size_t &pos) +{ + while(pos < hdr.size()) { + char *dt = lang; + + if (!pos) { + /* skip any initial whitespace. */ + while (pos < hdr.size() && xisspace(hdr[pos])) pos++; + } + else { + // IFF we terminated the tag on whitespace or ';' we need to skip to the next ',' or end of header. + while (pos < hdr.size() && hdr[pos] != ',') pos++; + if (hdr[pos] == ',') pos++; + } - textbuf.clean(); + /* + * Header value format: + * - sequence of whitespace delimited tags + * - each tag may suffix with ';'.* which we can ignore. + * - IFF a tag contains only two characters we can wildcard ANY translations matching: '-'? .* + * with preference given to an exact match. + */ + bool invalid_byte = false; + while (pos < hdr.size() && hdr[pos] != ';' && hdr[pos] != ',' && !xisspace(hdr[pos]) && dt < (lang + (langLen -1)) ) { + if (!invalid_byte) { +#if USE_HTTP_VIOLATIONS + // if accepting violations we may as well accept some broken browsers + // which may send us the right code, wrong ISO formatting. + if (hdr[pos] == '_') + *dt = '-'; + else +#endif + *dt = xtolower(hdr[pos]); + // valid codes only contain A-Z, hyphen (-) and * + if (*dt != '-' && *dt != '*' && (*dt < 'a' || *dt > 'z') ) + invalid_byte = true; + else + dt++; // move to next destination byte. + } + pos++; + } + *dt++ = '\0'; // nul-terminated the filename content string before system use. - return text; + debugs(4, 9, HERE << "STATE: dt='" << dt << "', lang='" << lang << "', pos=" << pos << ", buf='" << ((pos < hdr.size()) ? hdr.substr(pos,hdr.size()) : "") << "'"); + + /* if we found anything we might use, try it. */ + if (*lang != '\0' && !invalid_byte) + return true; + } + return false; +} + +bool +TemplateFile::loadFor(HttpRequest *request) +{ + String hdr; + + if (loaded()) // already loaded? + return true; + + if (!request || !request->header.getList(HDR_ACCEPT_LANGUAGE, &hdr) ) + return false; + + char lang[256]; + size_t pos = 0; // current parsing position in header string + + debugs(4, 6, HERE << "Testing Header: '" << hdr << "'"); + + while ( strHdrAcptLangGetItem(hdr, lang, 256, pos) ) { + + /* wildcard uses the configured default language */ + if (lang[0] == '*' && lang[1] == '\0') { + debugs(4, 6, HERE << "Found language '" << lang << "'. Using configured default."); + return false; + } + + debugs(4, 6, HERE << "Found language '" << lang << "', testing for available template"); + + if (tryLoadTemplate(lang)) { + /* store the language we found for the Content-Language reply header */ + errLanguage = lang; + break; + } else if (Config.errorLogMissingLanguages) { + debugs(4, DBG_IMPORTANT, "WARNING: Error Pages Missing Language: " << lang); + } + } + + return loaded(); } /// \ingroup ErrorPageInternal @@ -686,6 +810,7 @@ ErrorState::Convert(char token, bool building_deny_info_url, bool allowRecursion #if USE_SSL // currently only SSL error details implemented else if (detail) { + detail->useRequest(request); const String &errDetail = detail->toString(); if (errDetail.defined()) { MemBuf *detail_mb = ConvertText(errDetail.termedBuf(), false); @@ -1076,103 +1201,21 @@ ErrorState::BuildContent() assert(page_id > ERR_NONE && page_id < error_page_count); #if USE_ERR_LOCALES - String hdr; - char dir[256]; - int l = 0; - const char *freePage = NULL; + ErrorPageFile *localeTmpl = NULL; /** error_directory option in squid.conf overrides translations. * Custom errors are always found either in error_directory or the templates directory. * Otherwise locate the Accept-Language header */ - if (!Config.errorDirectory && page_id < ERR_MAX && request && request->header.getList(HDR_ACCEPT_LANGUAGE, &hdr) ) { - - size_t pos = 0; // current parsing position in header string - char *reset = NULL; // where to reset the p pointer for each new tag file - char *dt = NULL; - - /* prep the directory path string to prevent snprintf ... */ - l = strlen(DEFAULT_SQUID_ERROR_DIR); - memcpy(dir, DEFAULT_SQUID_ERROR_DIR, l); - dir[ l++ ] = '/'; - reset = dt = dir + l; - - debugs(4, 6, HERE << "Testing Header: '" << hdr << "'"); - - while ( pos < hdr.size() ) { - - /* skip any initial whitespace. */ - while (pos < hdr.size() && xisspace(hdr[pos])) pos++; - - /* - * Header value format: - * - sequence of whitespace delimited tags - * - each tag may suffix with ';'.* which we can ignore. - * - IFF a tag contains only two characters we can wildcard ANY translations matching: '-'? .* - * with preference given to an exact match. - */ - bool invalid_byte = false; - while (pos < hdr.size() && hdr[pos] != ';' && hdr[pos] != ',' && !xisspace(hdr[pos]) && dt < (dir+256) ) { - if (!invalid_byte) { -#if USE_HTTP_VIOLATIONS - // if accepting violations we may as well accept some broken browsers - // which may send us the right code, wrong ISO formatting. - if (hdr[pos] == '_') - *dt = '-'; - else -#endif - *dt = xtolower(hdr[pos]); - // valid codes only contain A-Z, hyphen (-) and * - if (*dt != '-' && *dt != '*' && (*dt < 'a' || *dt > 'z') ) - invalid_byte = true; - else - dt++; // move to next destination byte. - } - pos++; - } - *dt++ = '\0'; // nul-terminated the filename content string before system use. - - debugs(4, 9, HERE << "STATE: dt='" << dt << "', reset='" << reset << "', pos=" << pos << ", buf='" << ((pos < hdr.size()) ? hdr.substr(pos,hdr.size()) : "") << "'"); - - /* if we found anything we might use, try it. */ - if (*reset != '\0' && !invalid_byte) { - - /* wildcard uses the configured default language */ - if (reset[0] == '*' && reset[1] == '\0') { - debugs(4, 6, HERE << "Found language '" << reset << "'. Using configured default."); - m = error_text[page_id]; - if (!Config.errorDirectory) - err_language = Config.errorDefaultLanguage; - break; - } - - debugs(4, 6, HERE << "Found language '" << reset << "', testing for available template in: '" << dir << "'"); - - m = errorTryLoadText( err_type_str[page_id], dir, false); - - if (m) { - /* store the language we found for the Content-Language reply header */ - err_language = xstrdup(reset); - freePage = m; - break; - } else if (Config.errorLogMissingLanguages) { - debugs(4, DBG_IMPORTANT, "WARNING: Error Pages Missing Language: " << reset); - } - -#if HAVE_GLOB - if ( (dt - reset) == 2) { - /* TODO glob the error directory for sub-dirs matching: '-*' */ - /* use first result. */ - debugs(4,2, HERE << "wildcard fallback errors not coded yet."); - } -#endif - } - - dt = reset; // reset for next tag testing. we replace the failed name instead of cloning. - - // IFF we terminated the tag on whitespace or ';' we need to skip to the next ',' or end of header. - while (pos < hdr.size() && hdr[pos] != ',') pos++; - if (hdr[pos] == ',') pos++; + if (!Config.errorDirectory && page_id < ERR_MAX) { + if (err_language && err_language != Config.errorDefaultLanguage) + safe_free(err_language); + + localeTmpl = new ErrorPageFile(err_type_str[page_id]); + if (localeTmpl->loadFor(request)) { + m = localeTmpl->text(); + assert(localeTmpl->language()); + err_language = xstrdup(localeTmpl->language()); } } #endif /* USE_ERR_LOCALES */ @@ -1192,9 +1235,9 @@ ErrorState::BuildContent() MemBuf *result = ConvertText(m, true); #if USE_ERR_LOCALES - safe_free(freePage); + if (localeTmpl) + delete localeTmpl; #endif - return result; } diff --git a/src/errorpage.h b/src/errorpage.h index ddc3c6d353..a87662f477 100644 --- a/src/errorpage.h +++ b/src/errorpage.h @@ -40,6 +40,7 @@ #endif #include "cbdata.h" #include "ip/Address.h" +#include "MemBuf.h" #if USE_SSL #include "ssl/ErrorDetail.h" #endif @@ -248,4 +249,71 @@ SQUIDCEXTERN err_type errorReservePageId(const char *page_name); SQUIDCEXTERN ErrorState *errorCon(err_type type, http_status, HttpRequest * request); SQUIDCEXTERN const char *errorPageName(int pageId); ///< error ID to string +/** + \ingroup ErrorPageAPI + * + * loads text templates used for error pages and details; + * supports translation of templates + */ +class TemplateFile { +public: + TemplateFile(const char *name); + virtual ~TemplateFile(){} + + /// return true if the data loaded from disk without any problem + bool loaded() const {return wasLoaded;} + + /** + * Load the page_name template from a file which probably exist at: + * (a) admin specified custom directory (error_directory) + * (b) default language translation directory (error_default_language) + * (c) English sub-directory where errors should ALWAYS exist + */ + bool loadDefault(); + + /** + * Load an error template for a given HTTP request. This function examines the + * Accept-Language header and select the first available template. If the default + * template selected (eg because of a "Accept-Language: *"), or not available + * template found this function return false. + */ + bool loadFor(HttpRequest *request); + + /** + * Load the file given by "path". It uses the "parse()" method. + * On success return true and sets the "defined" member + */ + bool loadFromFile(const char *path); + + /// The language used for the template + const char *language() {return errLanguage.termedBuf();} + + bool silent; ///< Whether to print error messages on cache.log file or not. It is user defined. + +protected: + /// Used to parse (if parsing required) the template data . + virtual bool parse(const char *buf, int len, bool eof) = 0; + + /** + * Try to load the "page_name" template for a given language "lang" + * from squid errors directory + \return true on success false otherwise + */ + bool tryLoadTemplate(const char *lang); + + bool wasLoaded; ///< True if the template data read from disk without any problem + String errLanguage; ///< The error language of the template. + String templateName; ///< The name of the template +}; + +/** + * Parses the Accept-Language header value and return one language item on + * each call. + * \param hdr is the Accept-Language header value + * \param lang a buffer given by the user to store parsed language + * \param langlen the length of the lang buffer + * \param pos it is used to store the state of parsing. Must be "0" on first call + * \return true on success, false otherwise + */ +bool strHdrAcptLangGetItem(const String &hdr, char *lang, int langLen, size_t &pos); #endif /* SQUID_ERRORPAGE_H */ diff --git a/src/ssl/ErrorDetail.cc b/src/ssl/ErrorDetail.cc index 7c44cf774e..e3171205a1 100644 --- a/src/ssl/ErrorDetail.cc +++ b/src/ssl/ErrorDetail.cc @@ -1,175 +1,114 @@ #include "squid.h" +#include "errorpage.h" #include "ssl/ErrorDetail.h" #if HAVE_MAP #include #endif -struct SslErrorDetailEntry { +struct SslErrorEntry{ Ssl::ssl_error_t value; const char *name; - const char *detail; ///< for error page %D macro expansion; may contain macros - const char *descr; ///< short error description (for use in debug messages or error pages) }; static const char *SslErrorDetailDefaultStr = "SSL certificate validation error (%err_name): %ssl_subject"; //Use std::map to optimize search -typedef std::map SslErrorDetails; -SslErrorDetails TheSslDetail; +typedef std::map SslErrors; +SslErrors TheSslErrors; -static SslErrorDetailEntry TheSslDetailArray[] = { +static SslErrorEntry TheSslErrorArray[] = { {X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT, - "X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT", - "SSL Certficate error: certificate issuer (CA) not known: %ssl_ca_name", - "Unable to get issuer certificate"}, + "X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT"}, {X509_V_ERR_UNABLE_TO_GET_CRL, - "X509_V_ERR_UNABLE_TO_GET_CRL", - "%ssl_error_descr: %ssl_subject", - "Unable to get certificate CRL"}, + "X509_V_ERR_UNABLE_TO_GET_CRL"}, {X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE, - "X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE", - "%ssl_error_descr: %ssl_subject", - "Unable to decrypt certificate's signature"}, + "X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE"}, {X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE, - "X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE", - "%ssl_error_descr: %ssl_subject", - "Unable to decrypt CRL's signature"}, + "X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE"}, {X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY, - "X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", - "Unable to decode issuer (CA) public key: %ssl_ca_name", - "Unable to decode issuer public key"}, + "X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY"}, {X509_V_ERR_CERT_SIGNATURE_FAILURE, - "X509_V_ERR_CERT_SIGNATURE_FAILURE", - "%ssl_error_descr: %ssl_subject", - "Certificate signature failure"}, + "X509_V_ERR_CERT_SIGNATURE_FAILURE"}, {X509_V_ERR_CRL_SIGNATURE_FAILURE, - "X509_V_ERR_CRL_SIGNATURE_FAILURE", - "%ssl_error_descr: %ssl_subject", - "CRL signature failure"}, + "X509_V_ERR_CRL_SIGNATURE_FAILURE"}, {X509_V_ERR_CERT_NOT_YET_VALID, - "X509_V_ERR_CERT_NOT_YET_VALID", - "SSL Certficate is not valid before: %ssl_notbefore", - "Certificate is not yet valid"}, + "X509_V_ERR_CERT_NOT_YET_VALID"}, {X509_V_ERR_CERT_HAS_EXPIRED, - "X509_V_ERR_CERT_HAS_EXPIRED", - "SSL Certificate expired on: %ssl_notafter", - "Certificate has expired"}, + "X509_V_ERR_CERT_HAS_EXPIRED"}, {X509_V_ERR_CRL_NOT_YET_VALID, - "X509_V_ERR_CRL_NOT_YET_VALID", - "%ssl_error_descr: %ssl_subject", - "CRL is not yet valid"}, + "X509_V_ERR_CRL_NOT_YET_VALID"}, {X509_V_ERR_CRL_HAS_EXPIRED, - "X509_V_ERR_CRL_HAS_EXPIRED", - "%ssl_error_descr: %ssl_subject", - "CRL has expired"}, + "X509_V_ERR_CRL_HAS_EXPIRED"}, {X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD, - "X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD", - "SSL Certificate has invalid start date (the 'not before' field): %ssl_subject", - "Format error in certificate's notBefore field"}, + "X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD"}, {X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD, - "X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD", - "SSL Certificate has invalid expiration date (the 'not after' field): %ssl_subject", - "Format error in certificate's notAfter field"}, + "X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD"}, {X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD, - "X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD", - "%ssl_error_descr: %ssl_subject", - "Format error in CRL's lastUpdate field"}, + "X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD"}, {X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD, - "X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD", - "%ssl_error_descr: %ssl_subject", - "Format error in CRL's nextUpdate field"}, + "X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD"}, {X509_V_ERR_OUT_OF_MEM, - "X509_V_ERR_OUT_OF_MEM", - "%ssl_error_descr", - "Out of memory"}, + "X509_V_ERR_OUT_OF_MEM"}, {X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT, - "X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT", - "Self-signed SSL Certificate: %ssl_subject", - "Self signed certificate"}, + "X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT"}, {X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN, - "X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN", - "Self-signed SSL Certificate in chain: %ssl_subject", - "Self signed certificate in certificate chain"}, + "X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN"}, {X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, - "X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY", - "SSL Certficate error: certificate issuer (CA) not known: %ssl_ca_name", - "Unable to get local issuer certificate"}, + "X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY"}, {X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE, - "X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE", - "%ssl_error_descr: %ssl_subject", - "Unable to verify the first certificate"}, + "X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE"}, {X509_V_ERR_CERT_CHAIN_TOO_LONG, - "X509_V_ERR_CERT_CHAIN_TOO_LONG", - "%ssl_error_descr: %ssl_subject", - "Certificate chain too long"}, + "X509_V_ERR_CERT_CHAIN_TOO_LONG"}, {X509_V_ERR_CERT_REVOKED, - "X509_V_ERR_CERT_REVOKED", - "%ssl_error_descr: %ssl_subject", - "Certificate revoked"}, + "X509_V_ERR_CERT_REVOKED"}, {X509_V_ERR_INVALID_CA, - "X509_V_ERR_INVALID_CA", - "%ssl_error_descr: %ssl_ca_name", - "Invalid CA certificate"}, + "X509_V_ERR_INVALID_CA"}, {X509_V_ERR_PATH_LENGTH_EXCEEDED, - "X509_V_ERR_PATH_LENGTH_EXCEEDED", - "%ssl_error_descr: %ssl_subject", - "Path length constraint exceeded"}, + "X509_V_ERR_PATH_LENGTH_EXCEEDED"}, {X509_V_ERR_INVALID_PURPOSE, - "X509_V_ERR_INVALID_PURPOSE", - "%ssl_error_descr: %ssl_subject", - "Unsupported certificate purpose"}, + "X509_V_ERR_INVALID_PURPOSE"}, {X509_V_ERR_CERT_UNTRUSTED, - "X509_V_ERR_CERT_UNTRUSTED", - "%ssl_error_descr: %ssl_subject", - "Certificate not trusted"}, + "X509_V_ERR_CERT_UNTRUSTED"}, {X509_V_ERR_CERT_REJECTED, - "X509_V_ERR_CERT_REJECTED", - "%ssl_error_descr: %ssl_subject", - "Certificate rejected"}, + "X509_V_ERR_CERT_REJECTED"}, {X509_V_ERR_SUBJECT_ISSUER_MISMATCH, - "X509_V_ERR_SUBJECT_ISSUER_MISMATCH", - "%ssl_error_descr: %ssl_ca_name", - "Subject issuer mismatch"}, + "X509_V_ERR_SUBJECT_ISSUER_MISMATCH"}, {X509_V_ERR_AKID_SKID_MISMATCH, - "X509_V_ERR_AKID_SKID_MISMATCH", - "%ssl_error_descr: %ssl_subject", - "Authority and subject key identifier mismatch"}, + "X509_V_ERR_AKID_SKID_MISMATCH"}, {X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH, - "X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH", - "%ssl_error_descr: %ssl_ca_name", - "Authority and issuer serial number mismatch"}, + "X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH"}, {X509_V_ERR_KEYUSAGE_NO_CERTSIGN, - "X509_V_ERR_KEYUSAGE_NO_CERTSIGN", - "%ssl_error_descr: %ssl_subject", - "Key usage does not include certificate signing"}, + "X509_V_ERR_KEYUSAGE_NO_CERTSIGN"}, {X509_V_ERR_APPLICATION_VERIFICATION, - "X509_V_ERR_APPLICATION_VERIFICATION", - "%ssl_error_descr: %ssl_subject", - "Application verification failure"}, - { SSL_ERROR_NONE, "SSL_ERROR_NONE", "No error", "No error" }, - {SSL_ERROR_NONE, NULL, NULL, NULL } + "X509_V_ERR_APPLICATION_VERIFICATION"}, + { SSL_ERROR_NONE, "SSL_ERROR_NONE"}, + {SSL_ERROR_NONE, NULL} }; -static void loadSslDetailMap() +static void loadSslErrorMap() { - assert(TheSslDetail.empty()); - for (int i = 0; TheSslDetailArray[i].name; ++i) { - TheSslDetail[TheSslDetailArray[i].value] = &TheSslDetailArray[i]; + assert(TheSslErrors.empty()); + for (int i = 0; TheSslErrorArray[i].name; ++i) { + TheSslErrors[TheSslErrorArray[i].value] = &TheSslErrorArray[i]; } } +Ssl::ssl_error_t Ssl::GetErrorCode(const char *name) +{ + for (int i = 0; TheSslErrorArray[i].name != NULL; i++) { + if (strcmp(name, TheSslErrorArray[i].name) == 0) + return TheSslErrorArray[i].value; + } + return SSL_ERROR_NONE; +} + Ssl::ssl_error_t Ssl::ParseErrorString(const char *name) { assert(name); - if (TheSslDetail.empty()) - loadSslDetailMap(); - - typedef SslErrorDetails::const_iterator SEDCI; - for (SEDCI i = TheSslDetail.begin(); i != TheSslDetail.end(); ++i) { - if (strcmp(name, i->second->name) == 0) - return i->second->value; - } + const Ssl::ssl_error_t ssl_error = GetErrorCode(name); + if (ssl_error != SSL_ERROR_NONE) + return ssl_error; if (xisdigit(*name)) { const long int value = strtol(name, NULL, 0); @@ -182,44 +121,22 @@ Ssl::ParseErrorString(const char *name) return SSL_ERROR_SSL; // not reached } -static const SslErrorDetailEntry *getErrorRecord(Ssl::ssl_error_t value) +const char *Ssl::GetErrorName(Ssl::ssl_error_t value) { - if (TheSslDetail.empty()) - loadSslDetailMap(); + if (TheSslErrors.empty()) + loadSslErrorMap(); - const SslErrorDetails::const_iterator it = TheSslDetail.find(value); - if (it != TheSslDetail.end()) - return it->second; - - return NULL; -} - -const char * -Ssl::GetErrorName(Ssl::ssl_error_t value) -{ - if (const SslErrorDetailEntry *errorRecord = getErrorRecord(value)) - return errorRecord->name; + const SslErrors::const_iterator it = TheSslErrors.find(value); + if (it != TheSslErrors.end()) + return it->second->name; return NULL; } -static const char *getErrorDetail(Ssl::ssl_error_t value) -{ - if (const SslErrorDetailEntry *errorRecord = getErrorRecord(value)) - return errorRecord->detail; - - // we must always return something because ErrorDetail::buildDetail - // will hit an assertion - return SslErrorDetailDefaultStr; -} - const char * Ssl::GetErrorDescr(Ssl::ssl_error_t value) { - if (const SslErrorDetailEntry *errorRecord = getErrorRecord(value)) - return errorRecord->descr; - - return NULL; + return ErrorDetailsManager::GetInstance().getDefaultErrorDescr(value); } Ssl::ErrorDetail::err_frm_code Ssl::ErrorDetail::ErrorFormatingCodes[] = { @@ -320,7 +237,15 @@ const char *Ssl::ErrorDetail::notafter() const const char *Ssl::ErrorDetail::err_code() const { static char tmpBuffer[64]; - const char *err = GetErrorName(error_no); + // We can use the GetErrorName but using the detailEntry is faster, + // so try it first. + const char *err = detailEntry.name.termedBuf(); + + // error details not loaded yet or not defined in error_details.txt, + // try the GetErrorName... + if (!err) + err = GetErrorName(error_no); + if (!err) { snprintf(tmpBuffer, 64, "%d", (int)error_no); err = tmpBuffer; @@ -333,7 +258,9 @@ const char *Ssl::ErrorDetail::err_code() const */ const char *Ssl::ErrorDetail::err_descr() const { - if (const char *err = GetErrorDescr(error_no)) + if (error_no == SSL_ERROR_NONE) + return "[No Error]"; + if (const char *err = detailEntry.descr.termedBuf()) return err; return "[Not available]"; } @@ -372,11 +299,17 @@ int Ssl::ErrorDetail::convert(const char *code, const char **value) const */ void Ssl::ErrorDetail::buildDetail() const { - char const *s = getErrorDetail(error_no); + char const *s = NULL; char const *p; char const *t; int code_len = 0; + if (ErrorDetailsManager::GetInstance().getErrorDetail(error_no, request.raw(), detailEntry)) + s = detailEntry.detail.termedBuf(); + + if (!s) + s = SslErrorDetailDefaultStr; + assert(s); while ((p = strchr(s, '%'))) { errDetailStr.append(s, p - s); @@ -405,12 +338,17 @@ const String &Ssl::ErrorDetail::toString() const Ssl::ErrorDetail::ErrorDetail( Ssl::ssl_error_t err_no, X509 *cert): error_no (err_no) { peer_cert.reset(X509_dup(cert)); + detailEntry.error_no = SSL_ERROR_NONE; } Ssl::ErrorDetail::ErrorDetail(Ssl::ErrorDetail const &anErrDetail) { error_no = anErrDetail.error_no; + request = anErrDetail.request; + if (anErrDetail.peer_cert.get()) { peer_cert.reset(X509_dup(anErrDetail.peer_cert.get())); } + + detailEntry = anErrDetail.detailEntry; } diff --git a/src/ssl/ErrorDetail.h b/src/ssl/ErrorDetail.h index 32486bb88c..131f4318af 100644 --- a/src/ssl/ErrorDetail.h +++ b/src/ssl/ErrorDetail.h @@ -2,6 +2,8 @@ #define _SQUID_SSL_ERROR_DETAIL_H #include "err_detail_type.h" +#include "HttpRequest.h" +#include "ErrorDetailManager.h" #include "ssl/support.h" #include "ssl/gadgets.h" @@ -9,23 +11,21 @@ #include #endif -// Custom SSL errors; assumes all official errors are positive -#define SQUID_X509_V_ERR_DOMAIN_MISMATCH -1 -// All SSL errors range: from smallest (negative) custom to largest SSL error -#define SQUID_SSL_ERROR_MIN SQUID_X509_V_ERR_DOMAIN_MISMATCH -#define SQUID_SSL_ERROR_MAX INT_MAX - namespace Ssl { -/// Squid defined error code (<0), an error code returned by SSL X509 api, or SSL_ERROR_NONE -typedef int ssl_error_t; - /** - \ingroup ServerProtocolSSLAPI + \ingroup ServerProtocolSSLAPI * The ssl_error_t representation of the error described by "name". + * This function also parses numeric arguments. */ ssl_error_t ParseErrorString(const char *name); +/** + \ingroup ServerProtocolSSLAPI + * The ssl_error_t code of the error described by "name". + */ +ssl_error_t GetErrorCode(const char *name); + /** \ingroup ServerProtocolSSLAPI * The string representation of the SSL error "value" @@ -49,6 +49,7 @@ public: ErrorDetail(ssl_error_t err_no, X509 *cert); ErrorDetail(ErrorDetail const &); const String &toString() const; ///< An error detail string to embed in squid error pages + void useRequest(HttpRequest *aRequest) { if (request != NULL) request = aRequest;} /// The error name to embed in squid error pages const char *errorName() const {return err_code();} @@ -79,6 +80,8 @@ private: mutable String errDetailStr; ///< Caches the error detail message ssl_error_t error_no; ///< The error code X509_Pointer peer_cert; ///< A pointer to the peer certificate + mutable ErrorDetailEntry detailEntry; + HttpRequest::Pointer request; }; }//namespace Ssl diff --git a/src/ssl/ErrorDetailManager.cc b/src/ssl/ErrorDetailManager.cc new file mode 100644 index 0000000000..7cd8a3711e --- /dev/null +++ b/src/ssl/ErrorDetailManager.cc @@ -0,0 +1,249 @@ +#include "squid.h" +#include "ErrorDetail.h" +#include "errorpage.h" +#include "ErrorDetailManager.h" + +void Ssl::errorDetailInitialize() +{ + Ssl::ErrorDetailsManager::GetInstance(); +} + +void Ssl::errorDetailClean() +{ + Ssl::ErrorDetailsManager::Shutdown(); +} + +namespace Ssl +{ + +/// manages error detail templates +class ErrorDetailFile : public TemplateFile{ +public: + explicit ErrorDetailFile(ErrorDetailsList::Pointer const details): TemplateFile("error-details.txt") { + buf.init(); theDetails = details; + } + +private: + MemBuf buf; + ErrorDetailsList::Pointer theDetails; + virtual bool parse(const char *buf, int len, bool eof); +}; +}// namespace Ssl + +/******************/ +bool +Ssl::ErrorDetailsList::getRecord(Ssl::ssl_error_t value, ErrorDetailEntry &entry) +{ + const ErrorDetails::const_iterator it = theList.find(value); + if (it != theList.end()) { + entry.error_no = it->second.error_no; + entry.name = it->second.name; + entry.detail = it->second.detail; + entry.descr = it->second.descr; + return true; + } + return false; +} + +const char * +Ssl::ErrorDetailsList::getErrorDescr(Ssl::ssl_error_t value) +{ + const ErrorDetails::const_iterator it = theList.find(value); + if (it != theList.end()) { + return it->second.descr.termedBuf(); + } + + return NULL; +} + +const char * +Ssl::ErrorDetailsList::getErrorDetail(Ssl::ssl_error_t value) +{ + const ErrorDetails::const_iterator it = theList.find(value); + if (it != theList.end()) { + return it->second.detail.termedBuf(); + } + + return NULL; +} + +Ssl::ErrorDetailsManager *Ssl::ErrorDetailsManager::TheDetailsManager = NULL; + +Ssl::ErrorDetailsManager &Ssl::ErrorDetailsManager::GetInstance() +{ + if (!TheDetailsManager) + TheDetailsManager = new Ssl::ErrorDetailsManager; + + assert(TheDetailsManager); + return *TheDetailsManager; +} + +void Ssl::ErrorDetailsManager::Shutdown() +{ + delete TheDetailsManager; + TheDetailsManager = NULL; +} + + +Ssl::ErrorDetailsManager::ErrorDetailsManager() +{ + theDefaultErrorDetails = new ErrorDetailsList(); + ErrorDetailFile detailTmpl(theDefaultErrorDetails); + detailTmpl.loadDefault(); +} + +Ssl::ErrorDetailsList::Pointer Ssl::ErrorDetailsManager::getCachedDetails(const char *lang) +{ + Cache::iterator it; + it = cache.find(lang); + if (it != cache.end()) { + debugs(83, 8, HERE << "Found template details in cache for language: " << lang); + return it->second; + } + + return NULL; +} + +void Ssl::ErrorDetailsManager::cacheDetails(ErrorDetailsList::Pointer &errorDetails) +{ + const char *lang = errorDetails->errLanguage.termedBuf(); + assert(lang); + if (cache.find(lang) == cache.end()) + cache[lang] = errorDetails; +} + +bool +Ssl::ErrorDetailsManager::getErrorDetail(Ssl::ssl_error_t value, HttpRequest *request, ErrorDetailEntry &entry) +{ +#if USE_ERR_LOCALES + String hdr; + if (request && request->header.getList(HDR_ACCEPT_LANGUAGE, &hdr)) { + ErrorDetailsList::Pointer errDetails = NULL; + //Try to retrieve from cache + size_t pos = 0; + char lang[256]; + // Get the first ellement of the Accept-Language header + strHdrAcptLangGetItem(hdr, lang, 256, pos); + errDetails = getCachedDetails(lang); // search in cache + + if (!errDetails) { // Else try to load from disk + debugs(83, 8, HERE << "Creating new ErrDetailList to read from disk"); + errDetails = new ErrorDetailsList(); + ErrorDetailFile detailTmpl(errDetails); + if(detailTmpl.loadFor(request)) { + if (detailTmpl.language()) { + debugs(83, 8, HERE << "Found details on disk for language " << detailTmpl.language()); + errDetails->errLanguage = detailTmpl.language(); + cacheDetails(errDetails); + } + } + } + + if (errDetails != NULL && errDetails->getRecord(value, entry)) + return true; + } +#endif + + // else try the default + if (theDefaultErrorDetails->getRecord(value, entry)) { + debugs(83, 8, HERE << "Found default details record for error: " << GetErrorName(value)); + return true; + } + + return false; +} + +const char * +Ssl::ErrorDetailsManager::getDefaultErrorDescr(Ssl::ssl_error_t value) +{ + return theDefaultErrorDetails->getErrorDescr(value); +} + +const char * +Ssl::ErrorDetailsManager::getDefaultErrorDetail(Ssl::ssl_error_t value) +{ + return theDefaultErrorDetails->getErrorDetail(value); +} + +// Use HttpHeaders parser to parse error-details.txt files +class DetailEntryParser: public HttpHeader { +public: + DetailEntryParser():HttpHeader(hoErrorDetail) {} +}; + +//The end of an error detrail entry is a double "\n". The headersEnd +// functions can detect it +inline size_t detailEntryEnd(const char *s, size_t len) {return headersEnd(s, len);} + +bool +Ssl::ErrorDetailFile::parse(const char *buffer, int len, bool eof) +{ + if (!theDetails) + return false; + + if (len) { + buf.append(buffer, len); + } + + if (eof) + buf.append("\n\n", 1); + + while (size_t size = detailEntryEnd(buf.content(), buf.contentSize())) { + const char *e = buf.content() + size; + + //ignore spaces, new lines and comment lines (starting with #) at the beggining + const char *s; + for (s = buf.content(); (*s == '\n' || *s == ' ' || *s == '\t' || *s == '#') && s < e; s++) { + if(*s == '#') + while(sgetErrorDetail(ssl_error)) { + debugs(83, DBG_IMPORTANT, HERE << + "WARNING! duplicate entry: " << errorName); + return false; + } + + ErrorDetailEntry &entry = theDetails->theList[ssl_error]; + entry.error_no = ssl_error; + entry.name = errorName; + String tmp = parser.getByName("detail"); + httpHeaderParseQuotedString(tmp.termedBuf(), tmp.size(), &entry.detail); + tmp = parser.getByName("descr"); + httpHeaderParseQuotedString(tmp.termedBuf(), tmp.size(), &entry.descr); + bool parseOK = entry.descr.defined() && entry.detail.defined(); + + if (!parseOK) { + debugs(83, DBG_IMPORTANT, HERE << + "WARNING! missing imporant field for detail error: " << errorName); + return false; + } + }// else {only spaces and black lines; just ignore} + + buf.consume(size); + } + debugs(83, 9, HERE << " Remain size: " << buf.contentSize() << " Content: " << buf.content()); + return true; +} diff --git a/src/ssl/ErrorDetailManager.h b/src/ssl/ErrorDetailManager.h new file mode 100644 index 0000000000..2aeefc25ca --- /dev/null +++ b/src/ssl/ErrorDetailManager.h @@ -0,0 +1,87 @@ +#ifndef _SQUID_SSL_ERRORDETAILMANAGER_H +#define _SQUID_SSL_ERRORDETAILMANAGER_H + +#include "ssl/support.h" +#include "ssl/gadgets.h" +#if HAVE_MAP +#include +#endif +#if HAVE_STRING +#include +#endif + +namespace Ssl +{ + +class ErrorDetailEntry { +public: + Ssl::ssl_error_t error_no; ///< The SSL error code + String name; ///< a name for the error + String detail; ///< for error page %D macro expansion; may contain macros + String descr; ///< short error description (for use in debug messages or error pages) +}; + +/** + * Used to hold an error-details.txt template in ram. An error-details,.txt is represented + * by a list of error detail entries (ErrorDetailEntry objects). + */ +class ErrorDetailsList : public RefCountable +{ +public: + typedef RefCount Pointer; + /** + * Retrieves the error details for a given error to "entry" object + * \return true on success, false otherwise + */ + bool getRecord(Ssl::ssl_error_t value, ErrorDetailEntry &entry); + const char *getErrorDescr(Ssl::ssl_error_t value); ///< an error description for an error if exist in list. + const char *getErrorDetail(Ssl::ssl_error_t value); ///< an error details for an error if exist in list. + + String errLanguage; ///< The language of the error-details.txt template, if any + typedef std::map ErrorDetails; + ErrorDetails theList; ///< The list of error details entries +}; + +/** + * It is used to load, manage and query multiple ErrorDetailLists + * objects. + */ +class ErrorDetailsManager { +public: + ErrorDetailsManager(); + + static ErrorDetailsManager &GetInstance(); ///< Instance class + static void Shutdown(); ///< reset the ErrorDetailsManager instance + + /** + * Retrieve error details for an error. This method examine the Accept-Language + * of the request to retrieve the error details for requested language else return + * the default error details. + * \param vale the error code + * \param request the current HTTP request. + * \param entry where to store error details + * \return true on success, false otherwise + */ + bool getErrorDetail(Ssl::ssl_error_t value, HttpRequest *request, ErrorDetailEntry &entry); + const char *getDefaultErrorDescr(Ssl::ssl_error_t value); ///< the default error description for a given error + const char *getDefaultErrorDetail(Ssl::ssl_error_t value); ///< the default error details for a given error + +private: + /// Return cached error details list for a given language if exist + ErrorDetailsList::Pointer getCachedDetails(const char *lang); + /// cache the given error details list. + void cacheDetails(ErrorDetailsList::Pointer &errorDetails); + + typedef std::map Cache; + Cache cache; ///< the error details list cache + ErrorDetailsList::Pointer theDefaultErrorDetails; ///< the default error details list + + /// An instance of ErrorDetailsManager to be used by squid (ssl/ErrorDetails.*) + static ErrorDetailsManager *TheDetailsManager; +}; + + +void errorDetailInitialize(); +void errorDetailClean(); +} //namespace Ssl +#endif diff --git a/src/ssl/Makefile.am b/src/ssl/Makefile.am index 6eb6756a66..d8bad53191 100644 --- a/src/ssl/Makefile.am +++ b/src/ssl/Makefile.am @@ -23,6 +23,8 @@ libsslsquid_la_SOURCES = \ Config.h \ ErrorDetail.cc \ ErrorDetail.h \ + ErrorDetailManager.cc \ + ErrorDetailManager.h \ support.cc \ support.h diff --git a/src/ssl/support.h b/src/ssl/support.h index 4f03c7d7d9..eb24fd347d 100644 --- a/src/ssl/support.h +++ b/src/ssl/support.h @@ -55,6 +55,18 @@ \ingroup ServerProtocol */ +// Custom SSL errors; assumes all official errors are positive +#define SQUID_X509_V_ERR_DOMAIN_MISMATCH -1 +// All SSL errors range: from smallest (negative) custom to largest SSL error +#define SQUID_SSL_ERROR_MIN SQUID_X509_V_ERR_DOMAIN_MISMATCH +#define SQUID_SSL_ERROR_MAX INT_MAX + +namespace Ssl +{ +/// Squid defined error code (<0), an error code returned by SSL X509 api, or SSL_ERROR_NONE +typedef int ssl_error_t; +} //namespace Ssl + /// \ingroup ServerProtocolSSLAPI SSL_CTX *sslCreateServerContext(const char *certfile, const char *keyfile, int version, const char *cipher, const char *options, const char *flags, const char *clientCA, const char *CAfile, const char *CApath, const char *CRLfile, const char *dhpath, const char *context);