| /* |
| * Copyright 2014 Google Inc. |
| * |
| * Licensed 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. |
| */ |
| |
| // Author: kspoelstra@we-amp.com (Kees Spoelstra) |
| |
| #include "ngx_gzip_setter.h" |
| |
| #include <ngx_conf_file.h> |
| |
| namespace net_instaweb { |
| |
| NgxGZipSetter g_gzip_setter; |
| |
| extern "C" { |
| // These functions replace the setters for: |
| // gzip |
| // gzip_types |
| // gzip_http_version |
| // gzip_vary |
| // |
| // If these functions are called it means there is an explicit gzip |
| // configuration. The gzip configuration set by pagespeed is then rolled |
| // back and pagespeed will stop enabling gzip automatically. |
| char* ngx_gzip_redirect_conf_set_flag_slot( |
| ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { |
| if (g_gzip_setter.enabled()) { |
| g_gzip_setter.RollBackAndDisable(cf); |
| } |
| char* ret = ngx_conf_set_flag_slot(cf, cmd, conf); |
| return ret; |
| } |
| |
| char* ngx_gzip_redirect_http_types_slot( |
| ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { |
| if (g_gzip_setter.enabled()) { |
| g_gzip_setter.RollBackAndDisable(cf); |
| } |
| char* ret = ngx_http_types_slot(cf, cmd, conf); |
| return ret; |
| } |
| |
| char* ngx_gzip_redirect_conf_set_enum_slot( |
| ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { |
| if (g_gzip_setter.enabled()) { |
| g_gzip_setter.RollBackAndDisable(cf); |
| } |
| char* ret = ngx_conf_set_enum_slot(cf, cmd, conf); |
| return ret; |
| } |
| char* ngx_gzip_redirect_conf_set_bitmask_slot( |
| ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { |
| if (g_gzip_setter.enabled()) { |
| g_gzip_setter.RollBackAndDisable(cf); |
| } |
| char* ret = ngx_conf_set_bitmask_slot(cf, cmd, conf); |
| return ret; |
| } |
| } |
| |
| NgxGZipSetter::NgxGZipSetter() : enabled_(false), initialized_(false) { } |
| NgxGZipSetter::~NgxGZipSetter() { } |
| |
| // Helper functions to determine signature. |
| bool HasLocalConfig(ngx_command_t* command) { |
| return (!(command->type & (NGX_DIRECT_CONF|NGX_MAIN_CONF)) && |
| command->conf == NGX_HTTP_LOC_CONF_OFFSET); |
| } |
| bool IsNgxFlagCommand(ngx_command_t* command) { |
| return (command->set == ngx_conf_set_flag_slot && |
| HasLocalConfig(command)); |
| } |
| bool IsNgxHttpTypesCommand(ngx_command_t* command) { |
| return (command->set == ngx_http_types_slot && |
| HasLocalConfig(command)); |
| } |
| bool IsNgxEnumCommand(ngx_command_t* command) { |
| return (command->set == ngx_conf_set_enum_slot && |
| HasLocalConfig(command)); |
| } |
| bool IsNgxBitmaskCommand(ngx_command_t* command) { |
| return (command->set == ngx_conf_set_bitmask_slot && |
| HasLocalConfig(command)); |
| } |
| |
| // Initialize the NgxGZipSetter. |
| // Find the gzip, gzip_vary, gzip_http_version and gzip_types commands in the |
| // gzip module. Enable if the signature of the zip command matches with what we |
| // trust. Also sets up redirects for the configurations. These redirect handle |
| // a rollback if expicit configuration is found. |
| // If commands are not found the method will inform the user by logging. |
| void NgxGZipSetter::Init(ngx_conf_t* cf) { |
| #if (NGX_HTTP_GZIP) |
| bool gzip_signature_mismatch = false; |
| bool other_signature_mismatch = false; |
| // If we initialized already we don't have to scan again. |
| if (initialized_) { |
| // Config might have changed, so re-enable if we have gzip. |
| if (gzip_command_.command_ != NULL) { |
| enabled_ = true; |
| } else { |
| enabled_ = false; |
| } |
| return; |
| } |
| for (int m = 0; ngx_modules[m] != NULL; m++) { |
| if (ngx_modules[m]->commands != NULL) { |
| for (int c = 0; ngx_modules[m]->commands[c].name.len; c++) { |
| ngx_command_t* current_command =& ngx_modules[m]->commands[c]; |
| |
| // We look for the gzip command, and the exact signature we trust |
| // this means configured as an config location offset |
| // and a ngx_flag_t setter. |
| // Also see: |
| // ngx_conf_handler in ngx_conf_file.c |
| // ngx_http_gzip_filter_commands in ngx_http_gzip_filter.c |
| if (gzip_command_.command_ == NULL && |
| STR_EQ_LITERAL(current_command->name, "gzip")) { |
| if (IsNgxFlagCommand(current_command)) { |
| current_command->set = ngx_gzip_redirect_conf_set_flag_slot; |
| gzip_command_.command_ = current_command; |
| gzip_command_.module_ = ngx_modules[m]; |
| enabled_ = true; |
| } else { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, |
| "pagespeed: cannot set gzip, signature mismatch"); |
| gzip_signature_mismatch = true; |
| } |
| } |
| |
| if (!gzip_http_version_command_.command_ && |
| STR_EQ_LITERAL(current_command->name, "gzip_http_version")) { |
| if (IsNgxEnumCommand(current_command)) { |
| current_command->set = ngx_gzip_redirect_conf_set_enum_slot; |
| gzip_http_version_command_.command_ = current_command; |
| gzip_http_version_command_.module_ = ngx_modules[m]; |
| } else { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, |
| "pagespeed: cannot set gzip_http_version, signature mismatch"); |
| other_signature_mismatch = true; |
| } |
| } |
| |
| if (!gzip_proxied_command_.command_ && |
| STR_EQ_LITERAL(current_command->name, "gzip_proxied")) { |
| if (IsNgxBitmaskCommand(current_command)) { |
| current_command->set = ngx_gzip_redirect_conf_set_bitmask_slot; |
| gzip_proxied_command_.command_ = current_command; |
| gzip_proxied_command_.module_ = ngx_modules[m]; |
| } else { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, |
| "pagespeed: cannot set gzip_proxied, signature mismatch"); |
| other_signature_mismatch = true; |
| } |
| } |
| |
| if (!gzip_http_types_command_.command_ && |
| STR_EQ_LITERAL(current_command->name, "gzip_types")) { |
| if (IsNgxHttpTypesCommand(current_command)) { |
| current_command->set = ngx_gzip_redirect_http_types_slot; |
| gzip_http_types_command_.command_ = current_command; |
| gzip_http_types_command_.module_ = ngx_modules[m]; |
| } else { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, |
| "pagespeed: cannot set gzip_types, signature mismatch"); |
| other_signature_mismatch = true; |
| } |
| } |
| |
| if (!gzip_vary_command_.command_ && |
| STR_EQ_LITERAL(current_command->name, "gzip_vary")) { |
| if (IsNgxFlagCommand(current_command)) { |
| current_command->set = ngx_gzip_redirect_conf_set_flag_slot; |
| gzip_vary_command_.command_ = current_command; |
| gzip_vary_command_.module_ = ngx_modules[m]; |
| } else { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, |
| "pagespeed: cannot set gzip_vary, signature mismatch"); |
| other_signature_mismatch = true; |
| } |
| } |
| } |
| } |
| } |
| initialized_ = true; |
| if (gzip_signature_mismatch) { |
| return; // Already logged error. |
| } else if (!enabled_) { |
| // Looked through all the available commands and didn't find the "gzip" one. |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, "pagespeed: cannot set gzip, command not found"); |
| return; |
| } else if (other_signature_mismatch) { |
| return; // Already logged error. |
| } else if (!gzip_vary_command_.command_) { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, "pagespeed: missing gzip_vary"); |
| return; |
| } else if (!gzip_http_types_command_.command_) { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, "pagespeed: missing gzip_types"); |
| return; |
| } else if (!gzip_http_version_command_.command_) { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, "pagespeed: missing gzip_http_version"); |
| return; |
| } else if (!gzip_proxied_command_.command_) { |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, "pagespeed: missing gzip_proxied"); |
| return; |
| } else { |
| return; // Success. |
| } |
| #else |
| ngx_conf_log_error( |
| NGX_LOG_WARN, cf, 0, "pagespeed: gzip not compiled into nginx"); |
| return; |
| #endif |
| } |
| |
| void* ngx_command_ctx::GetConfPtr(ngx_conf_t* cf) { |
| return GetModuleConfPtr(cf) + command_->offset; |
| } |
| |
| char* ngx_command_ctx::GetModuleConfPtr(ngx_conf_t* cf) { |
| return reinterpret_cast<char*>( |
| ngx_http_conf_get_module_loc_conf(cf, (*(module_)))); |
| } |
| |
| void NgxGZipSetter::SetNgxConfFlag(ngx_conf_t* cf, |
| ngx_command_ctx* command_ctx, |
| ngx_flag_t value) { |
| ngx_flag_t* flag = reinterpret_cast<ngx_flag_t*>(command_ctx->GetConfPtr(cf)); |
| *flag = value; |
| // Save the flag position for possible rollback. |
| ngx_flags_set_.push_back(flag); |
| } |
| |
| void NgxGZipSetter::SetNgxConfEnum(ngx_conf_t* cf, |
| ngx_command_ctx* command_ctx, |
| ngx_uint_t value) { |
| ngx_uint_t* enum_to_set = |
| reinterpret_cast<ngx_uint_t*>(command_ctx->GetConfPtr(cf)); |
| *enum_to_set = value; |
| ngx_uint_set_.push_back(enum_to_set); |
| } |
| |
| void NgxGZipSetter::SetNgxConfBitmask(ngx_conf_t* cf, |
| ngx_command_ctx* command_ctx, |
| ngx_uint_t value) { |
| ngx_uint_t* enum_to_set = |
| reinterpret_cast<ngx_uint_t*>(command_ctx->GetConfPtr(cf)); |
| *enum_to_set = value; |
| ngx_uint_set_.push_back(enum_to_set); |
| } |
| |
| // These are the content types we want to compress. |
| ngx_str_t gzip_http_types[] = { |
| ngx_string("application/ecmascript"), |
| ngx_string("application/javascript"), |
| ngx_string("application/json"), |
| ngx_string("application/pdf"), |
| ngx_string("application/postscript"), |
| ngx_string("application/x-javascript"), |
| ngx_string("image/svg+xml"), |
| ngx_string("text/css"), |
| ngx_string("text/csv"), |
| // ngx_string("text/html"), // This is the default implied value. |
| ngx_string("text/javascript"), |
| ngx_string("text/plain"), |
| ngx_string("text/xml"), |
| ngx_null_string // Indicates end of array. |
| }; |
| |
| gzs_enable_result NgxGZipSetter::SetGZipForLocation(ngx_conf_t* cf, |
| bool value) { |
| if (!enabled_) { |
| return kEnableGZipNotEnabled; |
| } |
| if (gzip_command_.command_) { |
| SetNgxConfFlag(cf, &gzip_command_, value); |
| } |
| return kEnableGZipOk; |
| } |
| |
| void NgxGZipSetter::EnableGZipForLocation(ngx_conf_t* cf) { |
| if (!enabled_) { |
| return; |
| } |
| |
| // When we get called twice for the same location{}, we ignore the second call |
| // to prevent adding duplicate gzip http types and so on. |
| ngx_flag_t* flag = |
| reinterpret_cast<ngx_flag_t*>(gzip_command_.GetConfPtr(cf)); |
| if (*flag == 1) { |
| return; |
| } |
| SetGZipForLocation(cf, true); |
| if (gzip_vary_command_.command_) { |
| SetNgxConfFlag(cf, &gzip_vary_command_, 1); |
| } |
| if (gzip_http_version_command_.command_) { |
| SetNgxConfEnum(cf, &gzip_http_version_command_, NGX_HTTP_VERSION_10); |
| } |
| if (gzip_proxied_command_.command_) { |
| SetNgxConfBitmask( |
| cf, &gzip_http_version_command_, NGX_HTTP_GZIP_PROXIED_ANY); |
| } |
| |
| // This is actually the most prone to future API changes, because gzip_types |
| // is not a simple type like ngx_flag_t. The signature check should be enough |
| // to prevent problems. |
| AddGZipHTTPTypes(cf); |
| return; |
| } |
| |
| void NgxGZipSetter::AddGZipHTTPTypes(ngx_conf_t* cf) { |
| if (gzip_http_types_command_.command_) { |
| // Following should not happen, but if it does return gracefully. |
| if (cf->args->nalloc < 2) { |
| ngx_conf_log_error(NGX_LOG_WARN, cf, 0, |
| "pagespeed: unexpected small cf->args in gzip_types"); |
| return; |
| } |
| |
| ngx_command_t* command = gzip_http_types_command_.command_; |
| char* gzip_conf = reinterpret_cast<char* >( |
| gzip_http_types_command_.GetModuleConfPtr(cf)); |
| |
| // Backup the old settings. |
| ngx_str_t old_elt0 = reinterpret_cast<ngx_str_t*>(cf->args->elts)[0]; |
| ngx_str_t old_elt1 = reinterpret_cast<ngx_str_t*>(cf->args->elts)[1]; |
| ngx_uint_t old_nelts = cf->args->nelts; |
| |
| // Setup first arg. |
| ngx_str_t gzip_types_string = ngx_string("gzip_types"); |
| reinterpret_cast<ngx_str_t*>(cf->args->elts)[0] = gzip_types_string; |
| cf->args->nelts = 2; |
| |
| ngx_str_t* http_types = gzip_http_types; |
| while (http_types->data) { |
| ngx_str_t d; |
| // We allocate the http type on the configuration pool and actually |
| // leak this if we rollback. This does not seem to be a big problem, |
| // because nginx also allocates tokens in ngx_conf_file.c and does not |
| // free them. This way they can be used safely by configurations. |
| // We must use a copy of gzip_http_types array here because nginx will |
| // manipulate the values. |
| // TODO(kspoelstra): better would be to allocate once on init and not |
| // every time we enable gzip. This needs further investigation, sharing |
| // tokens might be problematic. |
| // For now I think it is not a large problem. This might add up in case |
| // of a large multi server/location config with a lot of "pagespeed on" |
| // directives. |
| // Estimates are 300-400KB for 1000 times "pagespeed on". |
| d.data = reinterpret_cast<u_char*>( |
| ngx_pnalloc(cf->pool, http_types->len + 1)); |
| snprintf(reinterpret_cast<char*>(d.data), http_types->len + 1, "%s", |
| reinterpret_cast<const char*>(http_types->data)); |
| d.len = http_types->len; |
| reinterpret_cast<ngx_str_t*>(cf->args->elts)[1] = d; |
| // Call the original setter. |
| ngx_http_types_slot(cf, command, gzip_conf); |
| http_types++; |
| } |
| |
| // Restore args. |
| cf->args->nelts = old_nelts; |
| reinterpret_cast<ngx_str_t*>(cf->args->elts)[1] = old_elt1; |
| reinterpret_cast<ngx_str_t*>(cf->args->elts)[0] = old_elt0; |
| |
| // Backup configuration location for rollback. |
| ngx_httptypes_set_.push_back(gzip_conf + command->offset); |
| } |
| } |
| |
| void NgxGZipSetter::RollBackAndDisable(ngx_conf_t* cf) { |
| ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, |
| "pagespeed: rollback gzip, explicit configuration"); |
| for (std::vector<ngx_flag_t*>::iterator i = ngx_flags_set_.begin(); |
| i != ngx_flags_set_.end(); ++i) { |
| *(*i)=NGX_CONF_UNSET; |
| } |
| for (std::vector<ngx_uint_t*>::iterator i = ngx_uint_set_.begin(); |
| i != ngx_uint_set_.end(); ++i) { |
| *(*i)=NGX_CONF_UNSET_UINT; |
| } |
| for (std::vector<void*>::iterator i = ngx_httptypes_set_.begin(); |
| i != ngx_httptypes_set_.end(); ++i) { |
| ngx_array_t** type_array = reinterpret_cast<ngx_array_t**>(*i); |
| ngx_array_destroy(*type_array); |
| *type_array = NULL; |
| } |
| enabled_ = 0; |
| } |
| |
| } // namespace net_instaweb |