| /** @file |
| |
| A brief file description |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| /**************************************************************************** |
| |
| HttpBodyFactory.cc |
| |
| |
| ****************************************************************************/ |
| |
| #include "tscore/ink_platform.h" |
| #include "tscore/ink_sprintf.h" |
| #include "tscore/ink_file.h" |
| #include "tscore/Filenames.h" |
| #include "HttpBodyFactory.h" |
| #include <unistd.h> |
| #include <dirent.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include "URL.h" |
| #include "logging/Log.h" |
| #include "logging/LogAccess.h" |
| #include "HttpCompat.h" |
| #include "tscore/I_Layout.h" |
| |
| ////////////////////////////////////////////////////////////////////// |
| // The HttpBodyFactory creates HTTP response page bodies, supported // |
| // configurable customization and language-targeting. // |
| // // |
| // The body factory can be reconfigured dynamically by a manager // |
| // callback, so locking is required. The callback takes a lock, // |
| // and the user entry points take a lock. These locks may limit // |
| // the speed of error page generation. // |
| ////////////////////////////////////////////////////////////////////// |
| |
| //////////////////////////////////////////////////////////////////////// |
| // |
| // User-Callable APIs --- locks will be taken internally |
| // |
| //////////////////////////////////////////////////////////////////////// |
| |
| char * |
| HttpBodyFactory::fabricate_with_old_api(const char *type, HttpTransact::State *context, int64_t max_buffer_length, |
| int64_t *resulting_buffer_length, char *content_language_out_buf, |
| size_t content_language_buf_size, char *content_type_out_buf, size_t content_type_buf_size, |
| int format_size, const char *format) |
| { |
| char *buffer = nullptr; |
| const char *lang_ptr = nullptr; |
| const char *charset_ptr = nullptr; |
| char url[1024]; |
| const char *set = nullptr; |
| bool found_requested_template = false; |
| bool plain_flag = false; |
| |
| /////////////////////////////////////////////// |
| // if suppressing this response, return NULL // |
| /////////////////////////////////////////////// |
| if (is_response_suppressed(context)) { |
| if (enable_logging) { |
| Log::error("BODY_FACTORY: suppressing '%s' response for url '%s'", type, url); |
| } |
| return nullptr; |
| } |
| |
| lock(); |
| |
| *resulting_buffer_length = 0; |
| |
| ink_strlcpy(content_language_out_buf, "en", content_language_buf_size); |
| ink_strlcpy(content_type_out_buf, "text/html", content_type_buf_size); |
| |
| /////////////////////////////////////////////////////////////////// |
| // if logging turned on, buffer up the URL string for simplicity // |
| /////////////////////////////////////////////////////////////////// |
| if (enable_logging) { |
| url[0] = '\0'; |
| URL *u = context->hdr_info.client_request.url_get(); |
| |
| if (u->valid()) { /* if url exists, copy the string into buffer */ |
| size_t i; |
| char *s = u->string_get(&context->arena); |
| for (i = 0; (i < sizeof(url) - 1) && s[i]; i++) { |
| url[i] = s[i]; |
| } |
| url[i] = '\0'; |
| if (s) { |
| context->arena.str_free(s); |
| } |
| } |
| } |
| ////////////////////////////////////////////////////////////////////////////////// |
| // if language-targeting activated, get client Accept-Language & Accept-Charset // |
| ////////////////////////////////////////////////////////////////////////////////// |
| |
| StrList acpt_language_list(false); |
| StrList acpt_charset_list(false); |
| |
| if (enable_customizations == 2) { |
| context->hdr_info.client_request.value_get_comma_list(MIME_FIELD_ACCEPT_LANGUAGE, MIME_LEN_ACCEPT_LANGUAGE, |
| &acpt_language_list); |
| context->hdr_info.client_request.value_get_comma_list(MIME_FIELD_ACCEPT_CHARSET, MIME_LEN_ACCEPT_CHARSET, &acpt_charset_list); |
| } |
| /////////////////////////////////////////// |
| // check if we don't need to format body // |
| /////////////////////////////////////////// |
| |
| buffer = (format == nullptr) ? nullptr : ats_strndup(format, format_size); |
| if (buffer != nullptr && format_size > 0) { |
| *resulting_buffer_length = format_size > max_buffer_length ? 0 : format_size; |
| plain_flag = true; |
| } |
| ///////////////////////////////////////////////////////// |
| // try to fabricate the desired type of error response // |
| ///////////////////////////////////////////////////////// |
| if (buffer == nullptr) { |
| buffer = |
| fabricate(&acpt_language_list, &acpt_charset_list, type, context, resulting_buffer_length, &lang_ptr, &charset_ptr, &set); |
| found_requested_template = (buffer != nullptr); |
| } |
| ///////////////////////////////////////////////////////////// |
| // if failed, try to fabricate the default custom response // |
| ///////////////////////////////////////////////////////////// |
| if (buffer == nullptr) { |
| if (is_response_body_precluded(context->http_return_code)) { |
| *resulting_buffer_length = 0; |
| unlock(); |
| return nullptr; |
| } |
| buffer = fabricate(&acpt_language_list, &acpt_charset_list, "default", context, resulting_buffer_length, &lang_ptr, |
| &charset_ptr, &set); |
| } |
| |
| /////////////////////////////////// |
| // enforce the max buffer length // |
| /////////////////////////////////// |
| if (buffer && (*resulting_buffer_length > max_buffer_length)) { |
| if (enable_logging) { |
| Log::error(("BODY_FACTORY: template '%s/%s' consumed %" PRId64 " bytes, " |
| "exceeding %" PRId64 " byte limit, using internal default"), |
| set, type, *resulting_buffer_length, max_buffer_length); |
| } |
| *resulting_buffer_length = 0; |
| buffer = static_cast<char *>(ats_free_null(buffer)); |
| } |
| ///////////////////////////////////////////////////////////////////// |
| // handle return of instantiated template and generate the content // |
| // language and content type return values // |
| ///////////////////////////////////////////////////////////////////// |
| if (buffer) { // got an instantiated template |
| if (!plain_flag) { |
| snprintf(content_language_out_buf, content_language_buf_size, "%s", lang_ptr); |
| snprintf(content_type_out_buf, content_type_buf_size, "text/html; charset=%s", charset_ptr); |
| } |
| |
| if (enable_logging) { |
| if (found_requested_template) { // got exact template |
| Log::error(("BODY_FACTORY: using custom template " |
| "'%s/%s' for url '%s' (language '%s', charset '%s')"), |
| set, type, url, lang_ptr, charset_ptr); |
| } else { // got default template |
| Log::error(("BODY_FACTORY: can't find custom template " |
| "'%s/%s', using '%s/%s' for url '%s' (language '%s', charset '%s')"), |
| set, type, set, "default", url, lang_ptr, charset_ptr); |
| } |
| } |
| } else { // no template |
| if (enable_logging) { |
| Log::error(("BODY_FACTORY: can't find templates '%s' or '%s' for url `%s'"), type, "default", url); |
| } |
| } |
| unlock(); |
| |
| return buffer; |
| } |
| |
| void |
| HttpBodyFactory::dump_template_tables(FILE *fp) |
| { |
| lock(); |
| if (table_of_sets) { |
| for (const auto &it1 : *table_of_sets.get()) { |
| HttpBodySet *body_set = static_cast<HttpBodySet *>(it1.second); |
| if (body_set) { |
| fprintf(fp, "set %s: name '%s', lang '%s', charset '%s'\n", it1.first.c_str(), body_set->set_name, |
| body_set->content_language, body_set->content_charset); |
| |
| /////////////////////////////////////////// |
| // loop over body-types->body hash table // |
| /////////////////////////////////////////// |
| |
| ink_assert(body_set->is_sane()); |
| if (body_set->table_of_pages) { |
| for (const auto &it2 : *body_set->table_of_pages.get()) { |
| fprintf(fp, " %-30s: %" PRId64 " bytes\n", it2.first.c_str(), it2.second->byte_count); |
| } |
| } |
| } |
| } |
| } |
| |
| unlock(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////// |
| // |
| // Configuration Change Callback |
| // |
| //////////////////////////////////////////////////////////////////////// |
| |
| static int |
| config_callback(const char * /* name ATS_UNUSED */, RecDataT /* data_type ATS_UNUSED */, RecData /* data ATS_UNUSED */, |
| void *cookie) |
| { |
| HttpBodyFactory *body_factory = static_cast<HttpBodyFactory *>(cookie); |
| body_factory->reconfigure(); |
| return 0; |
| } |
| |
| void |
| HttpBodyFactory::reconfigure() |
| { |
| RecInt e; |
| RecString s = nullptr; |
| bool all_found; |
| int rec_err; |
| |
| lock(); |
| sanity_check(); |
| |
| if (!callbacks_established) { |
| unlock(); |
| return; |
| } // callbacks not setup right |
| |
| //////////////////////////////////////////// |
| // extract relevant records.config values // |
| //////////////////////////////////////////// |
| |
| Debug("body_factory", "config variables changed, reconfiguring..."); |
| |
| all_found = true; |
| |
| // enable_customizations if records.config set |
| rec_err = RecGetRecordInt("proxy.config.body_factory.enable_customizations", &e); |
| enable_customizations = ((rec_err == REC_ERR_OKAY) ? e : 0); |
| all_found = all_found && (rec_err == REC_ERR_OKAY); |
| Debug("body_factory", "enable_customizations = %d (found = %" PRId64 ")", enable_customizations, e); |
| |
| rec_err = RecGetRecordInt("proxy.config.body_factory.enable_logging", &e); |
| enable_logging = ((rec_err == REC_ERR_OKAY) ? (e ? true : false) : false); |
| all_found = all_found && (rec_err == REC_ERR_OKAY); |
| Debug("body_factory", "enable_logging = %d (found = %" PRId64 ")", enable_logging, e); |
| |
| rec_err = RecGetRecordInt("proxy.config.body_factory.response_suppression_mode", &e); |
| response_suppression_mode = ((rec_err == REC_ERR_OKAY) ? e : 0); |
| all_found = all_found && (rec_err == REC_ERR_OKAY); |
| Debug("body_factory", "response_suppression_mode = %d (found = %" PRId64 ")", response_suppression_mode, e); |
| |
| ats_scoped_str directory_of_template_sets; |
| |
| rec_err = RecGetRecordString_Xmalloc("proxy.config.body_factory.template_sets_dir", &s); |
| all_found = all_found && (rec_err == REC_ERR_OKAY); |
| if (rec_err == REC_ERR_OKAY) { |
| directory_of_template_sets = Layout::get()->relative(s); |
| if (access(directory_of_template_sets, R_OK) < 0) { |
| Warning("Unable to access() directory '%s': %d, %s", (const char *)directory_of_template_sets, errno, strerror(errno)); |
| Warning(" Please set 'proxy.config.body_factory.template_sets_dir' "); |
| } |
| } |
| |
| Debug("body_factory", "directory_of_template_sets = '%s' ", (const char *)directory_of_template_sets); |
| |
| ats_free(s); |
| |
| if (!all_found) { |
| Warning("config changed, but can't fetch all proxy.config.body_factory values"); |
| } |
| |
| ///////////////////////////////////////////// |
| // clear out previous template hash tables // |
| ///////////////////////////////////////////// |
| |
| nuke_template_tables(); |
| |
| ///////////////////////////////////////////////////////////// |
| // at this point, the body hash table is gone, so we start // |
| // building a new one, by scanning the template directory. // |
| ///////////////////////////////////////////////////////////// |
| |
| if (directory_of_template_sets) { |
| table_of_sets = load_sets_from_directory(directory_of_template_sets); |
| } |
| |
| unlock(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////// |
| // |
| // class HttpBodyFactory |
| // |
| //////////////////////////////////////////////////////////////////////// |
| |
| HttpBodyFactory::HttpBodyFactory() |
| { |
| int i; |
| bool status, no_registrations_failed; |
| |
| //////////////////////////////////// |
| // initialize first-time defaults // |
| //////////////////////////////////// |
| ink_mutex_init(&mutex); |
| |
| ////////////////////////////////////////////////////// |
| // set up management configuration-change callbacks // |
| ////////////////////////////////////////////////////// |
| |
| static const char *config_record_names[] = { |
| "proxy.config.body_factory.enable_customizations", "proxy.config.body_factory.enable_logging", |
| "proxy.config.body_factory.template_sets_dir", "proxy.config.body_factory.response_suppression_mode", nullptr}; |
| |
| no_registrations_failed = true; |
| for (i = 0; config_record_names[i] != nullptr; i++) { |
| status = REC_RegisterConfigUpdateFunc(config_record_names[i], config_callback, (void *)this); |
| if (status != REC_ERR_OKAY) { |
| Warning("couldn't register variable '%s', is %s up to date?", config_record_names[i], ts::filename::RECORDS); |
| } |
| no_registrations_failed = no_registrations_failed && (status == REC_ERR_OKAY); |
| } |
| |
| if (no_registrations_failed == false) { |
| Warning("couldn't setup all body_factory callbacks, disabling body_factory"); |
| } else { |
| Debug("body_factory", "all callbacks established successfully"); |
| callbacks_established = true; |
| reconfigure(); |
| } |
| } |
| |
| HttpBodyFactory::~HttpBodyFactory() |
| { |
| // FIX: need to implement destructor |
| table_of_sets.reset(nullptr); |
| } |
| |
| // LOCKING: must be called with lock taken |
| char * |
| HttpBodyFactory::fabricate(StrList *acpt_language_list, StrList *acpt_charset_list, const char *type, HttpTransact::State *context, |
| int64_t *buffer_length_return, const char **content_language_return, const char **content_charset_return, |
| const char **set_return) |
| { |
| char *buffer; |
| const char *pType = context->txn_conf->body_factory_template_base; |
| const char *set; |
| char template_base[PATH_NAME_MAX]; |
| |
| if (set_return) { |
| *set_return = "???"; |
| } |
| *content_language_return = nullptr; |
| *content_charset_return = nullptr; |
| |
| Debug("body_factory", "calling fabricate(type '%s')", type); |
| *buffer_length_return = 0; |
| |
| // if error body suppressed, return NULL |
| if (is_response_suppressed(context)) { |
| Debug("body_factory", " error suppression enabled, returning NULL template"); |
| return nullptr; |
| } |
| // if custom error pages are disabled, return NULL |
| if (!enable_customizations) { |
| Debug("body_factory", " customization disabled, returning NULL template"); |
| return nullptr; |
| } |
| |
| // what set should we use (language target if enable_customizations == 2) |
| if (enable_customizations == 2) { |
| set = determine_set_by_language(acpt_language_list, acpt_charset_list); |
| } else if (enable_customizations == 3) { |
| set = determine_set_by_host(context); |
| } else if (is_response_body_precluded(context->http_return_code)) { |
| return nullptr; |
| } else { |
| set = "default"; |
| } |
| if (set_return) { |
| *set_return = set; |
| } |
| |
| HttpBodyTemplate *t = nullptr; |
| HttpBodySet *body_set = nullptr; |
| if (pType != nullptr && 0 != *pType && 0 != strncmp(pType, "NONE", 4)) { |
| sprintf(template_base, "%s_%s", pType, type); |
| t = find_template(set, template_base, &body_set); |
| // Check for default alternate. |
| if (t == nullptr) { |
| sprintf(template_base, "%s_default", pType); |
| t = find_template(set, template_base, &body_set); |
| } |
| } |
| |
| // Check for base customizations if specializations didn't work. |
| if (t == nullptr) { |
| if (is_response_body_precluded(context->http_return_code)) { |
| return nullptr; |
| } |
| t = find_template(set, type, &body_set); // this executes if the template_base is wrong and doesn't exist |
| } |
| |
| if (t == nullptr) { |
| Debug("body_factory", " can't find template, returning NULL template"); |
| return nullptr; |
| } |
| |
| *content_language_return = body_set->content_language; |
| *content_charset_return = body_set->content_charset; |
| |
| // build the custom error page |
| buffer = t->build_instantiated_buffer(context, buffer_length_return); |
| return buffer; |
| } |
| |
| // LOCKING: must be called with lock taken |
| const char * |
| HttpBodyFactory::determine_set_by_host(HttpTransact::State *context) |
| { |
| const char *set; |
| int host_len = context->hh_info.host_len; |
| char host_buffer[host_len + 1]; |
| strncpy(host_buffer, context->hh_info.request_host, host_len); |
| host_buffer[host_len] = '\0'; |
| if (auto it = table_of_sets->find(host_buffer); it != table_of_sets->end()) { |
| set = it->first.c_str(); |
| } else { |
| set = "default"; |
| } |
| return set; |
| } |
| |
| const char * |
| HttpBodyFactory::determine_set_by_language(std::unique_ptr<BodySetTable> &table_of_sets, StrList *acpt_language_list, |
| StrList *acpt_charset_list, float *Q_best_ptr, int *La_best_ptr, int *Lc_best_ptr, |
| int *I_best_ptr) |
| { |
| float Q, Ql, Qc, Q_best; |
| int I, Idummy, I_best; |
| int La, Lc, La_best, Lc_best; |
| int is_the_default_set; |
| const char *set_best; |
| |
| set_best = "default"; |
| Q_best = 0.00001; |
| La_best = 0; |
| Lc_best = INT_MAX; |
| I_best = INT_MAX; |
| |
| Debug("body_factory_determine_set", " INITIAL: [ set_best='%s', Q=%g, La=%d, Lc=%d, I=%d ]", set_best, Q_best, La_best, Lc_best, |
| I_best); |
| |
| // FIX: eliminate this special case (which doesn't work anyway), by properly |
| // handling empty lists and empty pieces in match_accept_XXX |
| |
| // if no Accept-Language or Accept-Charset, just return default |
| if ((acpt_language_list->count == 0) && (acpt_charset_list->count == 0)) { |
| Q_best = 1; |
| Debug("body_factory_determine_set", " no constraints => returning '%s'", set_best); |
| goto done; |
| } |
| |
| if (table_of_sets) { |
| /////////////////////////////////////////// |
| // loop over set->body-types hash table // |
| /////////////////////////////////////////// |
| for (const auto &it : *table_of_sets.get()) { |
| const char *set_name = it.first.c_str(); |
| HttpBodySetRawData *body_set = it.second; |
| |
| if ((it.first.empty()) || (body_set->table_of_pages == nullptr)) { |
| continue; |
| } |
| |
| ////////////////////////////////////////////////////////////////////// |
| // Take this error page language and match it against the // |
| // Accept-Language string passed in, to evaluate the match // |
| // quality. Disable wildcard processing so we use "default" // |
| // if no set explicitly matches. We also get back the index // |
| // of the match and the length of the match. // |
| // // |
| // We optimize the match in a couple of ways: // |
| // (a) if Q is better ==> wins, else if tie, // |
| // (b) if accept tag length La is bigger ==> wins, else if tie, // |
| // (c) if content tag length Lc is smaller ==> wins, else if tie, // |
| // (d) if index position I is smaller ==> wins // |
| ////////////////////////////////////////////////////////////////////// |
| |
| is_the_default_set = (strcmp(set_name, "default") == 0); |
| |
| Debug("body_factory_determine_set", " --- SET: %-8s (Content-Language '%s', Content-Charset '%s')", set_name, |
| body_set->content_language, body_set->content_charset); |
| |
| // if no Accept-Language hdr at all, treat as a wildcard that |
| // slightly prefers "default". |
| if (acpt_language_list->count == 0) { |
| Ql = (is_the_default_set ? 1.0001 : 1.000); |
| La = 0; |
| Lc = INT_MAX; |
| I = 1; |
| Debug("body_factory_determine_set", " SET: [%-8s] A-L not present => [ Ql=%g, La=%d, Lc=%d, I=%d ]", set_name, Ql, La, |
| Lc, I); |
| } else { |
| Lc = strlen(body_set->content_language); |
| Ql = HttpCompat::match_accept_language(body_set->content_language, Lc, acpt_language_list, &La, &I, true); |
| Debug("body_factory_determine_set", " SET: [%-8s] A-L match value => [ Ql=%g, La=%d, Lc=%d, I=%d ]", set_name, Ql, La, |
| Lc, I); |
| } |
| |
| ///////////////////////////////////////////////////////////// |
| // Take this error page language and match it against the // |
| // Accept-Charset string passed in, to evaluate the match // |
| // quality. Disable wildcard processing so that only // |
| // explicit values match. (Many browsers will send along // |
| // "*" with all lists, and we really don't want to send // |
| // strange character sets for these people --- we'd rather // |
| // use a more portable "default" set. The index value we // |
| // get back isn't used, because it's a little hard to know // |
| // how to tradeoff language indices vs. charset indices. // |
| // If someone cares, we could surely work charset indices // |
| // into the sorting computation below. // |
| ///////////////////////////////////////////////////////////// |
| |
| // if no Accept-Charset hdr at all, treat as a wildcard that |
| // slightly prefers "default". |
| if (acpt_charset_list->count == 0) { |
| Qc = (is_the_default_set ? 1.0001 : 1.000); |
| Idummy = 1; |
| Debug("body_factory_determine_set", " SET: [%-8s] A-C not present => [ Qc=%g ]", set_name, Qc); |
| } else { |
| Qc = HttpCompat::match_accept_charset(body_set->content_charset, strlen(body_set->content_charset), acpt_charset_list, |
| &Idummy, true); |
| Debug("body_factory_determine_set", " SET: [%-8s] A-C match value => [ Qc=%g ]", set_name, Qc); |
| } |
| |
| ///////////////////////////////////////////////////////////////// |
| // We get back the Q value, the matching field length, and the // |
| // matching field index. We sort by largest Q value, but if // |
| // there is a Q tie, we sub sort on longer matching length, // |
| // and if there is a tie on Q and L, we sub sort on position // |
| // index, preferring values earlier in Accept-Language list. // |
| ///////////////////////////////////////////////////////////////// |
| |
| Q = std::min(Ql, Qc); |
| |
| ////////////////////////////////////////////////////////// |
| // normally the Q for default pages should be slightly // |
| // less than for normal pages, but default pages should // |
| // always match to a slight level, in case everything // |
| // else doesn't match (matches with Q=0). // |
| ////////////////////////////////////////////////////////// |
| |
| if (is_the_default_set) { |
| Q = Q + -0.00005; |
| if (Q < 0.00001) { |
| Q = 0.00001; |
| } |
| } |
| |
| Debug("body_factory_determine_set", " NEW: [ set='%s', Q=%g, La=%d, Lc=%d, I=%d ]", set_name, Q, La, Lc, I); |
| Debug("body_factory_determine_set", " OLD: [ set='%s', Q=%g, La=%d, Lc=%d, I=%d ]", set_best, Q_best, La_best, Lc_best, |
| I_best); |
| |
| if (((Q > Q_best)) || ((Q == Q_best) && (La > La_best)) || ((Q == Q_best) && (La == La_best) && (Lc < Lc_best)) || |
| ((Q == Q_best) && (La == La_best) && (Lc == Lc_best) && (I < I_best))) { |
| Q_best = Q; |
| La_best = La; |
| Lc_best = Lc; |
| I_best = I; |
| set_best = set_name; |
| |
| Debug("body_factory_determine_set", " WINNER: [ set_best='%s', Q=%g, La=%d, Lc=%d, I=%d ]", set_best, Q_best, La_best, |
| Lc_best, I_best); |
| } else { |
| Debug("body_factory_determine_set", " LOSER: [ set_best='%s', Q=%g, La=%d, Lc=%d, I=%d ]", set_best, Q_best, La_best, |
| Lc_best, I_best); |
| } |
| } |
| } |
| |
| done: |
| |
| *Q_best_ptr = Q_best; |
| *La_best_ptr = La_best; |
| *Lc_best_ptr = Lc_best; |
| *I_best_ptr = I_best; |
| return set_best; |
| } |
| |
| // LOCKING: must be called with lock taken |
| const char * |
| HttpBodyFactory::determine_set_by_language(StrList *acpt_language_list, StrList *acpt_charset_list) |
| { |
| float Q_best; |
| const char *set_best; |
| int La_best, Lc_best, I_best; |
| |
| set_best = determine_set_by_language(table_of_sets, acpt_language_list, acpt_charset_list, &Q_best, &La_best, &Lc_best, &I_best); |
| |
| return set_best; |
| } |
| |
| // LOCKING: must be called with lock taken |
| HttpBodyTemplate * |
| HttpBodyFactory::find_template(const char *set, const char *type, HttpBodySet **body_set_return) |
| { |
| Debug("body_factory", "calling find_template(%s,%s)", set, type); |
| |
| *body_set_return = nullptr; |
| |
| if (table_of_sets == nullptr || !set || !type) { |
| return nullptr; |
| } |
| if (auto it = table_of_sets->find(set); it != table_of_sets->end()) { |
| HttpBodySet *body_set = static_cast<HttpBodySet *>(it->second); |
| if (body_set->table_of_pages == nullptr) { |
| return nullptr; |
| } |
| |
| if (auto it_page = body_set->table_of_pages->find(type); it_page != body_set->table_of_pages->end()) { |
| HttpBodyTemplate *t = it_page->second; |
| if ((t == nullptr) || (!t->is_sane())) { |
| return nullptr; |
| } |
| *body_set_return = body_set; |
| |
| Debug("body_factory", "find_template(%s,%s) -> (file %s, length %" PRId64 ", lang '%s', charset '%s')", set, type, |
| t->template_pathname, t->byte_count, body_set->content_language, body_set->content_charset); |
| |
| return t; |
| } |
| } |
| Debug("body_factory", "find_template(%s,%s) -> NULL", set, type); |
| |
| return nullptr; |
| } |
| |
| // LOCKING: must be called with lock taken |
| bool |
| HttpBodyFactory::is_response_suppressed(HttpTransact::State *context) |
| { |
| // Since a tunnel may not always be an SSL connection, |
| // we may want to return an error message. |
| // Even if it's an SSL connection, it won't cause any harm |
| // as the connection is going to be closed anyway. |
| /* |
| if (context->client_info.port_attribute == SERVER_PORT_BLIND_TUNNEL) { |
| // Blind SSL tunnels always supress error messages |
| return true; |
| } else |
| */ |
| if (response_suppression_mode == 0) { |
| return false; |
| } else if (response_suppression_mode == 1) { |
| return true; |
| } else if (response_suppression_mode == 2) { |
| return context->request_data.internal_txn; |
| } else { |
| return false; |
| } |
| } |
| |
| // LOCKING: must be called with lock taken |
| void |
| HttpBodyFactory::nuke_template_tables() |
| { |
| if (table_of_sets) { |
| Debug("body_factory", "deleting pre-existing template tables"); |
| } else { |
| Debug("body_factory", "no pre-existing template tables"); |
| } |
| if (table_of_sets) { |
| /////////////////////////////////////////// |
| // loop over set->body-types hash table // |
| /////////////////////////////////////////// |
| for (const auto &it : *table_of_sets.get()) { |
| HttpBodySet *body_set = static_cast<HttpBodySet *>(it.second); |
| ink_assert(body_set->is_sane()); |
| if (body_set->table_of_pages) { |
| /////////////////////////////////////////// |
| // loop over body-types->body hash table // |
| /////////////////////////////////////////// |
| for (const auto &it_page : *body_set->table_of_pages.get()) { |
| delete it_page.second; |
| } |
| body_set->table_of_pages.reset(nullptr); |
| } |
| delete body_set; |
| } |
| table_of_sets.reset(nullptr); |
| } |
| } |
| |
| // LOCKING: must be called with lock taken |
| std::unique_ptr<HttpBodyFactory::BodySetTable> |
| HttpBodyFactory::load_sets_from_directory(char *set_dir) |
| { |
| DIR *dir; |
| struct dirent *dirEntry; |
| std::unique_ptr<HttpBodyFactory::BodySetTable> new_table_of_sets; |
| |
| if (set_dir == nullptr) { |
| return nullptr; |
| } |
| |
| Debug("body_factory", "load_sets_from_directory(%s)", set_dir); |
| |
| ////////////////////////////////////////////////// |
| // try to open the requested template directory // |
| ////////////////////////////////////////////////// |
| |
| dir = opendir(set_dir); |
| if (dir == nullptr) { |
| Warning("can't open response template directory '%s' (%s)", set_dir, (strerror(errno) ? strerror(errno) : "unknown reason")); |
| Warning("no response templates --- using default error pages"); |
| return nullptr; |
| } |
| |
| new_table_of_sets.reset(new HttpBodyFactory::BodySetTable); |
| |
| ////////////////////////////////////////// |
| // loop over each language subdirectory // |
| ////////////////////////////////////////// |
| |
| while ((dirEntry = readdir(dir))) { |
| int status; |
| struct stat stat_buf; |
| char subdir[MAXPATHLEN + 1]; |
| |
| ////////////////////////////////////////////////////// |
| // ensure a subdirectory, and not starting with '.' // |
| ////////////////////////////////////////////////////// |
| |
| if ((dirEntry->d_name)[0] == '.') { |
| continue; |
| } |
| |
| ink_filepath_make(subdir, sizeof(subdir), set_dir, dirEntry->d_name); |
| status = stat(subdir, &stat_buf); |
| if (status != 0) { |
| continue; // can't stat |
| } |
| |
| if (!S_ISDIR(stat_buf.st_mode)) { |
| continue; // not a dir |
| } |
| |
| /////////////////////////////////////////////////////////// |
| // at this point, 'subdir' might be a valid template dir // |
| /////////////////////////////////////////////////////////// |
| |
| HttpBodySet *body_set = load_body_set_from_directory(dirEntry->d_name, subdir); |
| if (body_set != nullptr) { |
| Debug("body_factory", " %s -> %p", dirEntry->d_name, body_set); |
| new_table_of_sets->emplace(dirEntry->d_name, body_set); |
| } |
| } |
| |
| closedir(dir); |
| |
| return new_table_of_sets; |
| } |
| |
| // LOCKING: must be called with lock taken |
| HttpBodySet * |
| HttpBodyFactory::load_body_set_from_directory(char *set_name, char *tmpl_dir) |
| { |
| DIR *dir; |
| int status; |
| struct stat stat_buf; |
| struct dirent *dirEntry; |
| char path[MAXPATHLEN + 1]; |
| static const char BASED_DEFAULT[] = "_default"; |
| |
| //////////////////////////////////////////////// |
| // ensure we can open tmpl_dir as a directory // |
| //////////////////////////////////////////////// |
| |
| Debug("body_factory", " load_body_set_from_directory(%s)", tmpl_dir); |
| dir = opendir(tmpl_dir); |
| if (dir == nullptr) { |
| return nullptr; |
| } |
| |
| ///////////////////////////////////////////// |
| // ensure a .body_factory_info file exists // |
| ///////////////////////////////////////////// |
| |
| ink_filepath_make(path, sizeof(path), tmpl_dir, ".body_factory_info"); |
| status = stat(path, &stat_buf); |
| if ((status < 0) || !S_ISREG(stat_buf.st_mode)) { |
| Warning("Missing .body_factory_info in %s. Not loading body_factory templates", tmpl_dir); |
| closedir(dir); |
| return nullptr; |
| } |
| Debug("body_factory", " found '%s'", path); |
| |
| ///////////////////////////////////////////////////////////////// |
| // create body set, and loop over template files, loading them // |
| ///////////////////////////////////////////////////////////////// |
| |
| HttpBodySet *body_set = new HttpBodySet; |
| body_set->init(set_name, tmpl_dir); |
| |
| Debug("body_factory", " body_set = %p (set_name '%s', lang '%s', charset '%s')", body_set, body_set->set_name, |
| body_set->content_language, body_set->content_charset); |
| |
| while ((dirEntry = readdir(dir))) { |
| HttpBodyTemplate *tmpl; |
| size_t d_len = strlen(dirEntry->d_name); |
| |
| /////////////////////////////////////////////////////////////// |
| // all template files must have a file name of the form // |
| // - <type>#<subtype> // |
| // - <base>_<type>#<subtype> // |
| // - <base>_default [based default] // |
| // - default [global default] // |
| /////////////////////////////////////////////////////////////// |
| |
| if (!(nullptr != strchr(dirEntry->d_name, '#') || (0 == strcmp(dirEntry->d_name, "default")) || |
| (d_len >= sizeof(BASED_DEFAULT) && 0 == strcmp(dirEntry->d_name + d_len - (sizeof(BASED_DEFAULT) - 1), BASED_DEFAULT)))) { |
| continue; |
| } |
| |
| snprintf(path, sizeof(path), "%s/%s", tmpl_dir, dirEntry->d_name); |
| status = stat(path, &stat_buf); |
| if (status != 0) { |
| continue; // can't stat |
| } |
| |
| if (!S_ISREG(stat_buf.st_mode)) { |
| continue; // not a file |
| } |
| |
| //////////////////////////////// |
| // read in this template file // |
| //////////////////////////////// |
| |
| tmpl = new HttpBodyTemplate(); |
| if (!tmpl->load_from_file(tmpl_dir, dirEntry->d_name)) { |
| delete tmpl; |
| } else { |
| Debug("body_factory", " %s -> %p", dirEntry->d_name, tmpl); |
| body_set->set_template_by_name(dirEntry->d_name, tmpl); |
| } |
| } |
| |
| closedir(dir); |
| return body_set; |
| } |
| |
| //////////////////////////////////////////////////////////////////////// |
| // |
| // class HttpBodySet |
| // |
| //////////////////////////////////////////////////////////////////////// |
| |
| HttpBodySet::HttpBodySet() |
| { |
| magic = HTTP_BODY_SET_MAGIC; |
| |
| set_name = nullptr; |
| content_language = nullptr; |
| content_charset = nullptr; |
| |
| table_of_pages = nullptr; |
| } |
| |
| HttpBodySet::~HttpBodySet() |
| { |
| ats_free(set_name); |
| ats_free(content_language); |
| ats_free(content_charset); |
| table_of_pages.reset(nullptr); |
| } |
| |
| int |
| HttpBodySet::init(char *set, char *dir) |
| { |
| int fd, lineno, lines_added = 0; |
| char info_path[MAXPATHLEN]; |
| |
| char buffer[1024], name[1025], value[1024]; |
| |
| ink_filepath_make(info_path, sizeof(info_path), dir, ".body_factory_info"); |
| fd = open(info_path, O_RDONLY); |
| if (fd < 0) { |
| return -1; |
| } |
| |
| this->set_name = ats_strdup(set); |
| this->table_of_pages.reset(new TemplateTable); |
| |
| lineno = 0; |
| |
| while (true) { |
| char *name_s, *name_e, *value_s, *value_e, *hash; |
| |
| ++lineno; |
| int bytes = ink_file_fd_readline(fd, sizeof(buffer), buffer); |
| if (bytes <= 0) { |
| break; |
| } |
| |
| /////////////////////////////////////////////// |
| // chop anything on and after first '#' sign // |
| /////////////////////////////////////////////// |
| |
| hash = index(buffer, '#'); |
| if (hash) { |
| *hash = '\0'; |
| |
| //////////////////////////////// |
| // find start and end of name // |
| //////////////////////////////// |
| } |
| name_s = buffer; |
| while (*name_s && ParseRules::is_wslfcr(*name_s)) { |
| ++name_s; |
| } |
| |
| name_e = name_s; |
| while (*name_e && ParseRules::is_http_field_name(*name_e)) { |
| ++name_e; |
| } |
| |
| if (name_s == name_e) { |
| continue; // blank line |
| } |
| |
| ///////////////////////////////// |
| // find start and end of value // |
| ///////////////////////////////// |
| |
| value_s = name_e; |
| while (*value_s && (ParseRules::is_wslfcr(*value_s))) { |
| ++value_s; |
| } |
| if (*value_s != ':') { |
| Warning("ignoring invalid body factory info line #%d in %s", lineno, info_path); |
| continue; |
| } |
| ++value_s; // skip the colon |
| while (*value_s && (ParseRules::is_wslfcr(*value_s))) { |
| ++value_s; |
| } |
| value_e = buffer + strlen(buffer) - 1; |
| while ((value_e > value_s) && ParseRules::is_wslfcr(*(value_e - 1))) { |
| --value_e; |
| } |
| |
| ///////////////////////////////// |
| // insert line into hash table // |
| ///////////////////////////////// |
| |
| memcpy(name, name_s, name_e - name_s); |
| name[name_e - name_s] = '\0'; |
| |
| memcpy(value, value_s, value_e - value_s); |
| value[value_e - value_s] = '\0'; |
| |
| ////////////////////////////////////////////////// |
| // so far, we only support 2 pieces of metadata // |
| ////////////////////////////////////////////////// |
| |
| if (strcasecmp(name, "Content-Language") == 0) { |
| ats_free(this->content_language); |
| this->content_language = ats_strdup(value); |
| } else if (strcasecmp(name, "Content-Charset") == 0) { |
| ats_free(this->content_charset); |
| this->content_charset = ats_strdup(value); |
| } |
| } |
| |
| //////////////////////////////////////////////////// |
| // fill in default language & charset, if not set // |
| //////////////////////////////////////////////////// |
| |
| if (!this->content_language) { |
| if (strcmp(set, "default") == 0) { |
| this->content_language = ats_strdup("en"); |
| } else { |
| this->content_language = ats_strdup(set); |
| } |
| } |
| if (!this->content_charset) { |
| this->content_charset = ats_strdup("utf-8"); |
| } |
| |
| close(fd); |
| return lines_added; |
| } |
| |
| HttpBodyTemplate * |
| HttpBodySet::get_template_by_name(const char *name) |
| { |
| Debug("body_factory", " calling get_template_by_name(%s)", name); |
| |
| if (table_of_pages == nullptr) { |
| return nullptr; |
| } |
| |
| if (auto it = table_of_pages->find(name); it != table_of_pages->end()) { |
| HttpBodyTemplate *t = it->second; |
| if ((t == nullptr) || (!t->is_sane())) { |
| return nullptr; |
| } |
| Debug("body_factory", " get_template_by_name(%s) -> (file %s, length %" PRId64 ")", name, t->template_pathname, |
| t->byte_count); |
| return t; |
| } |
| Debug("body_factory", " get_template_by_name(%s) -> NULL", name); |
| return nullptr; |
| } |
| |
| void |
| HttpBodySet::set_template_by_name(const char *name, HttpBodyTemplate *t) |
| { |
| if (name) { |
| table_of_pages->emplace(name, t); |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////// |
| // |
| // class HttpBodyTemplate |
| // |
| //////////////////////////////////////////////////////////////////////// |
| |
| HttpBodyTemplate::HttpBodyTemplate() |
| { |
| magic = HTTP_BODY_TEMPLATE_MAGIC; |
| byte_count = 0; |
| template_buffer = nullptr; |
| template_pathname = nullptr; |
| } |
| |
| HttpBodyTemplate::~HttpBodyTemplate() |
| { |
| reset(); |
| } |
| |
| void |
| HttpBodyTemplate::reset() |
| { |
| ats_free(template_buffer); |
| template_buffer = nullptr; |
| byte_count = 0; |
| ats_free(template_pathname); |
| } |
| |
| int |
| HttpBodyTemplate::load_from_file(char *dir, char *file) |
| { |
| int fd, status; |
| int64_t bytes_read; |
| struct stat stat_buf; |
| char path[MAXPATHLEN + 1]; |
| char *new_template_buffer; |
| int64_t new_byte_count; |
| |
| //////////////////////////////////// |
| // ensure this is actually a file // |
| //////////////////////////////////// |
| |
| snprintf(path, sizeof(path), "%s/%s", dir, file); |
| // coverity[fs_check_call] |
| status = stat(path, &stat_buf); |
| if (status != 0) { |
| return 0; |
| } |
| if (!S_ISREG(stat_buf.st_mode)) { |
| return 0; |
| } |
| |
| /////////////////// |
| // open the file // |
| /////////////////// |
| |
| // coverity[toctou] |
| fd = open(path, O_RDONLY); |
| if (fd < 0) { |
| return 0; |
| } |
| |
| //////////////////////////////////////// |
| // read in the template file contents // |
| //////////////////////////////////////// |
| |
| new_byte_count = stat_buf.st_size; |
| new_template_buffer = static_cast<char *>(ats_malloc(new_byte_count + 1)); |
| bytes_read = read(fd, new_template_buffer, new_byte_count); |
| new_template_buffer[new_byte_count] = '\0'; |
| close(fd); |
| |
| /////////////////////////// |
| // check for read errors // |
| /////////////////////////// |
| |
| if (bytes_read != new_byte_count) { |
| Warning("reading template file '%s', got %" PRId64 " bytes instead of %" PRId64 " (%s)", path, bytes_read, new_byte_count, |
| (strerror(errno) ? strerror(errno) : "unknown error")); |
| ats_free(new_template_buffer); |
| return 0; |
| } |
| |
| Debug("body_factory", " read %" PRId64 " bytes from '%s'", new_byte_count, path); |
| |
| ///////////////////////////////// |
| // actually commit the changes // |
| ///////////////////////////////// |
| |
| reset(); |
| template_buffer = new_template_buffer; |
| byte_count = new_byte_count; |
| template_pathname = ats_strdup(path); |
| |
| return 1; |
| } |
| |
| char * |
| HttpBodyTemplate::build_instantiated_buffer(HttpTransact::State *context, int64_t *buflen_return) |
| { |
| char *buffer = nullptr; |
| |
| Debug("body_factory_instantiation", " before instantiation: [%s]", template_buffer); |
| |
| LogAccess la(context->state_machine); |
| |
| buffer = resolve_logfield_string(&la, template_buffer); |
| |
| *buflen_return = ((buffer == nullptr) ? 0 : strlen(buffer)); |
| Debug("body_factory_instantiation", " after instantiation: [%s]", buffer); |
| Debug("body_factory", " returning %" PRId64 " byte instantiated buffer", *buflen_return); |
| |
| return buffer; |
| } |