/* * 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 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_(0) { } 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; 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_ = 1; } 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; } } } } } 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( 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(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(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(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(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( gzip_http_types_command_.GetModuleConfPtr(cf)); // Backup the old settings. ngx_str_t old_elt0 = reinterpret_cast(cf->args->elts)[0]; ngx_str_t old_elt1 = reinterpret_cast(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(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( ngx_pnalloc(cf->pool, http_types->len + 1)); snprintf(reinterpret_cast(d.data), http_types->len + 1, "%s", reinterpret_cast(http_types->data)); d.len = http_types->len; reinterpret_cast(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(cf->args->elts)[1] = old_elt1; reinterpret_cast(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_INFO, cf, 0, "pagespeed: rollback gzip, explicit configuration"); for (std::vector::iterator i = ngx_flags_set_.begin(); i != ngx_flags_set_.end(); ++i) { *(*i)=NGX_CONF_UNSET; } for (std::vector::iterator i = ngx_uint_set_.begin(); i != ngx_uint_set_.end(); ++i) { *(*i)=NGX_CONF_UNSET_UINT; } for (std::vector::iterator i = ngx_httptypes_set_.begin(); i != ngx_httptypes_set_.end(); ++i) { ngx_array_t** type_array = reinterpret_cast(*i); ngx_array_destroy(*type_array); *type_array = NULL; } enabled_ = 0; } } // namespace net_instaweb