blob: 6e85bc36080347c7c4e952babd54cee1c4f96f5e [file] [log] [blame]
/*
* 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_(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<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_INFO, 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