| /* |
| * Copyright 2012 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: jefftk@google.com (Jeff Kaufman) |
| |
| /* |
| * Usage: |
| * server { |
| * pagespeed on|off; |
| * } |
| */ |
| |
| #include "ngx_pagespeed.h" |
| |
| #include <vector> |
| #include <set> |
| |
| #include "ngx_base_fetch.h" |
| #include "ngx_caching_headers.h" |
| #include "ngx_gzip_setter.h" |
| #include "ngx_list_iterator.h" |
| #include "ngx_message_handler.h" |
| #include "ngx_rewrite_driver_factory.h" |
| #include "ngx_rewrite_options.h" |
| #include "ngx_server_context.h" |
| |
| #include "net/instaweb/automatic/public/proxy_fetch.h" |
| #include "net/instaweb/http/public/async_fetch.h" |
| #include "net/instaweb/http/public/cache_url_async_fetcher.h" |
| #include "net/instaweb/http/public/content_type.h" |
| #include "net/instaweb/http/public/request_context.h" |
| #include "net/instaweb/public/global_constants.h" |
| #include "net/instaweb/public/version.h" |
| #include "net/instaweb/rewriter/public/experiment_matcher.h" |
| #include "net/instaweb/rewriter/public/experiment_util.h" |
| #include "net/instaweb/rewriter/public/process_context.h" |
| #include "net/instaweb/rewriter/public/resource_fetch.h" |
| #include "net/instaweb/rewriter/public/rewrite_driver.h" |
| #include "net/instaweb/rewriter/public/rewrite_options.h" |
| #include "net/instaweb/rewriter/public/rewrite_query.h" |
| #include "net/instaweb/rewriter/public/rewrite_stats.h" |
| #include "net/instaweb/rewriter/public/static_asset_manager.h" |
| #include "net/instaweb/system/public/in_place_resource_recorder.h" |
| #include "net/instaweb/system/public/system_caches.h" |
| #include "net/instaweb/system/public/system_request_context.h" |
| #include "net/instaweb/system/public/system_rewrite_options.h" |
| #include "net/instaweb/system/public/system_server_context.h" |
| #include "net/instaweb/system/public/system_thread_system.h" |
| #include "net/instaweb/util/public/fallback_property_page.h" |
| #include "net/instaweb/util/public/google_message_handler.h" |
| #include "net/instaweb/util/public/google_url.h" |
| #include "net/instaweb/util/public/gzip_inflater.h" |
| #include "net/instaweb/util/public/null_message_handler.h" |
| #include "net/instaweb/util/public/query_params.h" |
| #include "net/instaweb/util/public/statistics_logger.h" |
| #include "net/instaweb/util/public/stdio_file_system.h" |
| #include "net/instaweb/util/public/string.h" |
| #include "net/instaweb/util/public/string_writer.h" |
| #include "net/instaweb/util/public/time_util.h" |
| #include "net/instaweb/util/stack_buffer.h" |
| #include "pagespeed/kernel/base/posix_timer.h" |
| #include "pagespeed/kernel/http/query_params.h" |
| #include "pagespeed/kernel/html/html_keywords.h" |
| #include "pagespeed/kernel/thread/pthread_shared_mem.h" |
| |
| extern ngx_module_t ngx_pagespeed; |
| |
| // Unused flag, see |
| // http://lxr.evanmiller.org/http/source/http/ngx_http_request.h#L130 |
| #define NGX_HTTP_PAGESPEED_BUFFERED 0x08 |
| #define POST_BUF_READ_SIZE 65536 |
| |
| // Needed for SystemRewriteDriverFactory to use shared memory. |
| #define PAGESPEED_SUPPORT_POSIX_SHARED_MEM |
| |
| namespace net_instaweb { |
| |
| const char* kInternalEtagName = "@psol-etag"; |
| bool factory_init_called = false; |
| |
| // The process context takes care of proactively initialising |
| // a few libraries for us, some of which are not thread-safe |
| // when they are initialized lazily. |
| ProcessContext* process_context = new ProcessContext(); |
| bool process_context_cleanup_hooked = false; |
| |
| StringPiece str_to_string_piece(ngx_str_t s) { |
| return StringPiece(reinterpret_cast<char*>(s.data), s.len); |
| } |
| |
| // Nginx uses memory pools, like Apache, allocating strings a request needs from |
| // the pool and then throwing it all away when the request finishes. Use this |
| // when you need to pass a short string to nginx and want it to take ownership |
| // of the string. |
| char* string_piece_to_pool_string(ngx_pool_t* pool, StringPiece sp) { |
| // Need space for the final null. |
| ngx_uint_t buffer_size = sp.size() + 1; |
| char* s = static_cast<char*>(ngx_palloc(pool, buffer_size)); |
| if (s == NULL) { |
| LOG(ERROR) << "string_piece_to_pool_string: ngx_palloc() returned NULL"; |
| DCHECK(false); |
| return NULL; |
| } |
| sp.copy(s, buffer_size /* max to copy */); |
| s[buffer_size-1] = '\0'; // Null terminate it. |
| return s; |
| } |
| |
| // When passing the body of http responses between filters Nginx uses a linked |
| // list of buffers ("buffer chain"), again like Apache. This constructs one of |
| // those lists from a StringPiece. This is what you use when you need to pass a |
| // (potentially) longer string to nginx and want it to take ownership. |
| ngx_int_t string_piece_to_buffer_chain( |
| ngx_pool_t* pool, StringPiece sp, ngx_chain_t** link_ptr, |
| bool send_last_buf) { |
| // Below, *link_ptr will be NULL if we're starting the chain, and the head |
| // chain link. |
| *link_ptr = NULL; |
| |
| // If non-null, the current last link in the chain. |
| ngx_chain_t* tail_link = NULL; |
| |
| // How far into sp we're currently working on. |
| ngx_uint_t offset; |
| |
| // Other modules seem to default to ngx_pagesize. |
| ngx_uint_t max_buffer_size = ngx_pagesize; |
| for (offset = 0 ; |
| offset < sp.size() || |
| // If we need to send the last buffer bit and there's no data, we |
| // should send a single empty buffer. Otherwise we shouldn't |
| // generate empty buffers. |
| (offset == 0 && sp.size() == 0); |
| offset += max_buffer_size) { |
| // Prepare a new nginx buffer to put our buffered writes into. |
| ngx_buf_t* b = static_cast<ngx_buf_t*>(ngx_calloc_buf(pool)); |
| if (b == NULL) { |
| return NGX_ERROR; |
| } |
| |
| if (sp.size() == 0) { |
| CHECK(offset == 0); // NOLINT |
| b->pos = b->start = b->end = b->last = NULL; |
| // The purpose of this buffer is just to pass along last_buf. |
| b->sync = 1; |
| } else { |
| CHECK(sp.size() > offset); |
| ngx_uint_t b_size = sp.size() - offset; |
| if (b_size > max_buffer_size) { |
| b_size = max_buffer_size; |
| } |
| |
| b->start = b->pos = static_cast<u_char*>(ngx_palloc(pool, b_size)); |
| if (b->pos == NULL) { |
| return NGX_ERROR; |
| } |
| |
| // Copy our writes over. We're copying from sp[offset] up to |
| // sp[offset + b_size] into b which has size b_size. |
| sp.copy(reinterpret_cast<char*>(b->pos), b_size, offset); |
| b->last = b->end = b->pos + b_size; |
| |
| b->temporary = 1; // Identify this buffer as in-memory and mutable. |
| } |
| |
| // Prepare a chain link. |
| ngx_chain_t* cl = static_cast<ngx_chain_t*>(ngx_alloc_chain_link(pool)); |
| if (cl == NULL) { |
| return NGX_ERROR; |
| } |
| |
| cl->buf = b; |
| cl->next = NULL; |
| |
| if (*link_ptr == NULL) { |
| // This is the first link in the returned chain. |
| *link_ptr = cl; |
| } else { |
| // Link us into the chain. |
| CHECK(tail_link != NULL); |
| tail_link->next = cl; |
| } |
| |
| tail_link = cl; |
| } |
| |
| |
| CHECK(tail_link != NULL); |
| if (send_last_buf) { |
| tail_link->buf->last_buf = true; |
| } |
| |
| return NGX_OK; |
| } |
| |
| namespace { |
| |
| // Setting headers in nginx is tricky because it's not just a matter of adding |
| // them to a list. You also need to remove them if there's already one there, |
| // as well as setting the shortcut pointers (both upper case and lower case). |
| // |
| // Based on ngx_http_add_cache_control. |
| ngx_int_t ps_set_cache_control(ngx_http_request_t* r, char* cache_control) { |
| // First strip existing cache-control headers. |
| ngx_table_elt_t* header; |
| NgxListIterator it(&(r->headers_out.headers.part)); |
| while ((header = it.Next()) != NULL) { |
| if (STR_CASE_EQ_LITERAL(header->key, "Cache-Control")) { |
| // Response headers with hash of 0 are excluded from the response. |
| header->hash = 0; |
| } |
| } |
| |
| // Now add our new cache control header. |
| if (r->headers_out.cache_control.elts == NULL) { |
| ngx_int_t rc = ngx_array_init(&r->headers_out.cache_control, r->pool, |
| 1, sizeof(ngx_table_elt_t*)); |
| if (rc != NGX_OK) { |
| return NGX_ERROR; |
| } |
| } |
| ngx_table_elt_t** cache_control_headers = static_cast<ngx_table_elt_t**>( |
| ngx_array_push(&r->headers_out.cache_control)); |
| if (cache_control_headers == NULL) { |
| return NGX_ERROR; |
| } |
| cache_control_headers[0] = static_cast<ngx_table_elt_t*>( |
| ngx_list_push(&r->headers_out.headers)); |
| if (cache_control_headers[0] == NULL) { |
| return NGX_ERROR; |
| } |
| cache_control_headers[0]->hash = 1; |
| ngx_str_set(&cache_control_headers[0]->key, "Cache-Control"); |
| cache_control_headers[0]->value.len = strlen(cache_control); |
| cache_control_headers[0]->value.data = |
| reinterpret_cast<u_char*>(cache_control); |
| |
| return NGX_OK; |
| } |
| |
| template<class Headers> |
| void copy_headers_from_table(const ngx_list_t &from, Headers* to) { |
| // Standard nginx idiom for iterating over a list. See ngx_list.h |
| ngx_uint_t i; |
| const ngx_list_part_t* part = &from.part; |
| const ngx_table_elt_t* header = static_cast<ngx_table_elt_t*>(part->elts); |
| |
| for (i = 0 ; /* void */; i++) { |
| if (i >= part->nelts) { |
| if (part->next == NULL) { |
| break; |
| } |
| |
| part = part->next; |
| header = static_cast<ngx_table_elt_t*>(part->elts); |
| i = 0; |
| } |
| // Make sure we don't copy over headers that are unset. |
| if (header[i].hash == 0) { |
| continue; |
| } |
| StringPiece key = str_to_string_piece(header[i].key); |
| StringPiece value = str_to_string_piece(header[i].value); |
| |
| to->Add(key, value); |
| } |
| } |
| } // namespace |
| |
| void copy_response_headers_from_ngx(const ngx_http_request_t* r, |
| ResponseHeaders* headers) { |
| headers->set_major_version(r->http_version / 1000); |
| headers->set_minor_version(r->http_version % 1000); |
| copy_headers_from_table(r->headers_out.headers, headers); |
| |
| headers->set_status_code(r->headers_out.status); |
| |
| // Manually copy over the content type because it's not included in |
| // request_->headers_out.headers. |
| headers->Add(HttpAttributes::kContentType, |
| str_to_string_piece(r->headers_out.content_type)); |
| |
| // When we don't have a date header, set one with the current time. |
| if (headers->Lookup1(HttpAttributes::kDate) == NULL) { |
| PosixTimer timer; |
| headers->SetDate(timer.NowMs()); |
| } |
| |
| // TODO(oschaaf): ComputeCaching should be called in setupforhtml()? |
| headers->ComputeCaching(); |
| } |
| |
| void copy_request_headers_from_ngx(const ngx_http_request_t* r, |
| RequestHeaders* headers) { |
| // TODO(chaizhenhua): only allow RewriteDriver::kPassThroughRequestAttributes? |
| headers->set_major_version(r->http_version / 1000); |
| headers->set_minor_version(r->http_version % 1000); |
| copy_headers_from_table(r->headers_in.headers, headers); |
| } |
| |
| // PSOL produces caching headers that need some changes before we can send them |
| // out. Make those changes and populate r->headers_out from pagespeed_headers. |
| ngx_int_t copy_response_headers_to_ngx( |
| ngx_http_request_t* r, |
| const ResponseHeaders& pagespeed_headers, |
| PreserveCachingHeaders preserve_caching_headers) { |
| ngx_http_headers_out_t* headers_out = &r->headers_out; |
| headers_out->status = pagespeed_headers.status_code(); |
| |
| ngx_int_t i; |
| for (i = 0 ; i < pagespeed_headers.NumAttributes() ; i++) { |
| const GoogleString& name_gs = pagespeed_headers.Name(i); |
| const GoogleString& value_gs = pagespeed_headers.Value(i); |
| |
| if (preserve_caching_headers == kPreserveAllCachingHeaders) { |
| if (StringCaseEqual(name_gs, "ETag") || |
| StringCaseEqual(name_gs, "Expires") || |
| StringCaseEqual(name_gs, "Date") || |
| StringCaseEqual(name_gs, "Last-Modified") || |
| StringCaseEqual(name_gs, "Cache-Control")) { |
| continue; |
| } |
| } else if (preserve_caching_headers == kPreserveOnlyCacheControl) { |
| // Retain the original Cache-Control header, but send the recomputed |
| // values for all other cache-related headers. |
| if (StringCaseEqual(name_gs, "Cache-Control")) { |
| continue; |
| } |
| } // else we don't preserve any headers. |
| |
| ngx_str_t name, value; |
| |
| // If the gzip module is not configured, we must not rename the header, |
| // because we will fail to inject the header filter that will rename the |
| // header back. |
| bool gzip_enabled = false; |
| #if (NGX_HTTP_GZIP) |
| gzip_enabled = true; |
| #endif |
| // To prevent the gzip module from clearing weak etags, we output them |
| // using a different name here. The etag header filter module runs behind |
| // the gzip compressors header filter, and will rename it to 'ETag' |
| if (gzip_enabled && StringCaseEqual(name_gs, "etag") |
| && StringCaseStartsWith(value_gs, "W/")) { |
| name.len = strlen(kInternalEtagName); |
| name.data = reinterpret_cast<u_char*>( |
| const_cast<char*>(kInternalEtagName)); |
| } else { |
| name.len = name_gs.length(); |
| name.data = reinterpret_cast<u_char*>(const_cast<char*>(name_gs.data())); |
| } |
| |
| value.len = value_gs.length(); |
| value.data = reinterpret_cast<u_char*>(const_cast<char*>(value_gs.data())); |
| |
| // TODO(jefftk): If we're setting a cache control header we'd like to |
| // prevent any downstream code from changing it. Specifically, if we're |
| // serving a cache-extended resource the url will change if the resource |
| // does and so we've given it a long lifetime. If the site owner has done |
| // something like set all css files to a 10-minute cache lifetime, that |
| // shouldn't apply to our generated resources. See Apache code in |
| // net/instaweb/apache/header_util:AddResponseHeadersToRequest |
| |
| // Make copies of name and value to put into headers_out. |
| |
| u_char* value_s = ngx_pstrdup(r->pool, &value); |
| if (value_s == NULL) { |
| return NGX_ERROR; |
| } |
| |
| if (STR_EQ_LITERAL(name, "Cache-Control")) { |
| ps_set_cache_control(r, const_cast<char*>(value_gs.c_str())); |
| continue; |
| } else if (STR_EQ_LITERAL(name, "Content-Type")) { |
| // Unlike all the other headers, content_type is just a string. |
| headers_out->content_type.data = value_s; |
| headers_out->content_type.len = value.len; |
| |
| // We should not include the charset when determining content_type_len, so |
| // scan for the ';' that marks the start of the charset part. |
| for (ngx_uint_t i = 0; i < value.len; i++) { |
| if (value_s[i] == ';') { |
| break; |
| } |
| headers_out->content_type_len = i + 1; |
| } |
| |
| // In ngx_http_test_content_type() nginx will allocate and calculate |
| // content_type_lowcase if we leave it as null. |
| headers_out->content_type_lowcase = NULL; |
| continue; |
| // TODO(oschaaf): are there any other headers we should not try to |
| // copy here? |
| } else if (STR_EQ_LITERAL(name, "Connection")) { |
| continue; |
| } else if (STR_EQ_LITERAL(name, "Keep-Alive")) { |
| continue; |
| } else if (STR_EQ_LITERAL(name, "Transfer-Encoding")) { |
| continue; |
| } |
| |
| u_char* name_s = ngx_pstrdup(r->pool, &name); |
| if (name_s == NULL) { |
| return NGX_ERROR; |
| } |
| |
| ngx_table_elt_t* header = static_cast<ngx_table_elt_t*>( |
| ngx_list_push(&headers_out->headers)); |
| if (header == NULL) { |
| return NGX_ERROR; |
| } |
| |
| header->hash = 1; // Include this header in the output. |
| header->key.len = name.len; |
| header->key.data = name_s; |
| header->value.len = value.len; |
| header->value.data = value_s; |
| |
| // Populate the shortcuts to commonly used headers. |
| if (STR_EQ_LITERAL(name, "Date")) { |
| headers_out->date = header; |
| } else if (STR_EQ_LITERAL(name, "Etag")) { |
| headers_out->etag = header; |
| } else if (STR_EQ_LITERAL(name, "Expires")) { |
| headers_out->expires = header; |
| } else if (STR_EQ_LITERAL(name, "Last-Modified")) { |
| headers_out->last_modified = header; |
| } else if (STR_EQ_LITERAL(name, "Location")) { |
| headers_out->location = header; |
| } else if (STR_EQ_LITERAL(name, "Server")) { |
| headers_out->server = header; |
| } else if (STR_EQ_LITERAL(name, "Content-Length")) { |
| int64 len; |
| CHECK(pagespeed_headers.FindContentLength(&len)); |
| headers_out->content_length_n = len; |
| headers_out->content_length = header; |
| } |
| } |
| |
| return NGX_OK; |
| } |
| |
| namespace { |
| |
| typedef struct { |
| NgxRewriteDriverFactory* driver_factory; |
| MessageHandler* handler; |
| } ps_main_conf_t; |
| |
| typedef struct { |
| // If pagespeed is configured in some server block but not this one our |
| // per-request code will be invoked but server context will be null. In those |
| // cases we neet to short circuit, not changing anything. Currently our |
| // header filter, body filter, and content handler all do this, but if anyone |
| // adds another way for nginx to give us a request to process we need to check |
| // there as well. |
| NgxServerContext* server_context; |
| ProxyFetchFactory* proxy_fetch_factory; |
| // Only used while parsing config. After we merge cfg_s and cfg_m you most |
| // likely want cfg_s->server_context->config() as options here will be NULL. |
| NgxRewriteOptions* options; |
| MessageHandler* handler; |
| } ps_srv_conf_t; |
| |
| typedef struct { |
| NgxRewriteOptions* options; |
| MessageHandler* handler; |
| } ps_loc_conf_t; |
| |
| namespace RequestRouting { |
| enum Response { |
| kError, |
| kNotUnderstood, |
| kStaticContent, |
| kInvalidUrl, |
| kPagespeedDisabled, |
| kBeacon, |
| kStatistics, |
| kGlobalStatistics, |
| kConsole, |
| kMessages, |
| kAdmin, |
| kCachePurge, |
| kGlobalAdmin, |
| kPagespeedSubrequest, |
| kNotHeadOrGet, |
| kErrorResponse, |
| kResource, |
| }; |
| } // namespace RequestRouting |
| |
| char* ps_main_configure(ngx_conf_t* cf, ngx_command_t* cmd, void* conf); |
| char* ps_srv_configure(ngx_conf_t* cf, ngx_command_t* cmd, void* conf); |
| char* ps_loc_configure(ngx_conf_t* cf, ngx_command_t* cmd, void* conf); |
| |
| // TODO(jud): Verify that all the offsets should be NGX_HTTP_SRV_CONF_OFFSET and |
| // not NGX_HTTP_LOC_CONF_OFFSET or NGX_HTTP_MAIN_CONF_OFFSET. |
| ngx_command_t ps_commands[] = { |
| { ngx_string("pagespeed"), |
| NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1| |
| NGX_CONF_TAKE2|NGX_CONF_TAKE3|NGX_CONF_TAKE4|NGX_CONF_TAKE5, |
| ps_main_configure, |
| NGX_HTTP_SRV_CONF_OFFSET, |
| 0, |
| NULL }, |
| { ngx_string("pagespeed"), |
| NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1| |
| NGX_CONF_TAKE2|NGX_CONF_TAKE3|NGX_CONF_TAKE4|NGX_CONF_TAKE5, |
| ps_srv_configure, |
| NGX_HTTP_SRV_CONF_OFFSET, |
| 0, |
| NULL }, |
| |
| { ngx_string("pagespeed"), |
| NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1| |
| NGX_CONF_TAKE2|NGX_CONF_TAKE3|NGX_CONF_TAKE4|NGX_CONF_TAKE5, |
| ps_loc_configure, |
| NGX_HTTP_SRV_CONF_OFFSET, |
| 0, |
| NULL }, |
| |
| ngx_null_command |
| }; |
| |
| void ps_ignore_sigpipe() { |
| struct sigaction act; |
| ngx_memzero(&act, sizeof(act)); |
| act.sa_handler = SIG_IGN; |
| sigemptyset(&act.sa_mask); |
| act.sa_flags = 0; |
| sigaction(SIGPIPE, &act, NULL); |
| } |
| |
| // Given a directory path that pagespeed needs, create it and set permissions so |
| // the worker can access, but only if needed. |
| char* ps_init_dir(const StringPiece& directive, |
| const StringPiece& path, |
| ngx_conf_t* cf) { |
| if (path.size() == 0 || path[0] != '/') { |
| return string_piece_to_pool_string( |
| cf->pool, net_instaweb::StrCat(directive, " ", path, |
| " must start with a slash")); |
| } |
| |
| net_instaweb::StdioFileSystem file_system; |
| net_instaweb::NullMessageHandler message_handler; |
| GoogleString gs_path; |
| path.CopyToString(&gs_path); |
| if (!file_system.IsDir(gs_path.c_str(), &message_handler).is_true()) { |
| if (!file_system.RecursivelyMakeDir(path, &message_handler)) { |
| return string_piece_to_pool_string( |
| cf->pool, net_instaweb::StrCat( |
| directive, " path ", path, |
| " does not exist and could not be created.")); |
| } |
| // Directory created, but may not be readable by the worker processes. |
| } |
| |
| if (geteuid() != 0) { |
| return NULL; // We're not root, so we're staying whoever we are. |
| } |
| |
| // chown if owner differs from nginx worker user. |
| ngx_core_conf_t* ccf = reinterpret_cast<ngx_core_conf_t*>( |
| ngx_get_conf(cf->cycle->conf_ctx, ngx_core_module)); |
| CHECK(ccf != NULL); |
| struct stat gs_stat; |
| if (stat(gs_path.c_str(), &gs_stat) != 0) { |
| return string_piece_to_pool_string( |
| cf->pool, net_instaweb::StrCat( |
| directive, " ", path, " stat() failed")); |
| } |
| if (gs_stat.st_uid != ccf->user) { |
| if (chown(gs_path.c_str(), ccf->user, ccf->group) != 0) { |
| return string_piece_to_pool_string( |
| cf->pool, net_instaweb::StrCat( |
| directive, " ", path, " unable to set permissions")); |
| } |
| } |
| |
| return NULL; |
| } |
| |
| // We support interpretation of nginx variables in some configuration settings, |
| // but we also need to support literal dollar signs in those same settings. |
| // Nginx has no good solution for this, so we define $dollar to expand to '$', |
| // which lets people include literal dollar signs if they need them. |
| ngx_int_t ps_dollar( |
| ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { |
| v->valid = 1; |
| v->no_cacheable = 0; |
| v->not_found = 0; |
| v->data = reinterpret_cast<u_char*>(const_cast<char*>("$")); |
| v->len = 1; |
| return NGX_OK; |
| } |
| |
| // Parse the configuration option represented by cf and add it to options, |
| // creating options if necessary. |
| char* ps_configure(ngx_conf_t* cf, |
| NgxRewriteOptions** options, |
| MessageHandler* handler, |
| net_instaweb::RewriteOptions::OptionScope option_scope) { |
| ps_main_conf_t* cfg_m = static_cast<ps_main_conf_t*>( |
| ngx_http_conf_get_module_main_conf(cf, ngx_pagespeed)); |
| |
| if (!factory_init_called) { |
| // Init logging to nginx's default error_log. |
| cfg_m->driver_factory->LoggingInit(cf->cycle->log); |
| cfg_m->driver_factory->Init(); |
| factory_init_called = true; |
| } |
| |
| // args[0] is always "pagespeed"; ignore it. |
| ngx_uint_t n_args = cf->args->nelts - 1; |
| |
| // In ps_commands we only register 'pagespeed' as taking up to |
| // five arguments, so this check should never fire. |
| CHECK(n_args <= NGX_PAGESPEED_MAX_ARGS); |
| StringPiece args[NGX_PAGESPEED_MAX_ARGS]; |
| |
| ngx_str_t* value = static_cast<ngx_str_t*>(cf->args->elts); |
| ngx_uint_t i; |
| for (i = 0 ; i < n_args ; i++) { |
| args[i] = str_to_string_piece(value[i+1]); |
| } |
| |
| if (n_args == 1) { |
| if (StringCaseEqual(args[0], "on")) { |
| // safe to call if the setter is disabled |
| g_gzip_setter.EnableGZipForLocation(cf); |
| } else if (StringCaseEqual(args[0], "off")) { |
| g_gzip_setter.SetGZipForLocation(cf, false); |
| } |
| } |
| if (n_args == 2 && args[0].compare("gzip") == 0) { |
| if (args[1].compare("on") == 0) { |
| g_gzip_setter.SetGZipForLocation(cf, true); |
| } else if (args[1].compare("off") == 0) { |
| g_gzip_setter.SetGZipForLocation(cf, false); |
| } else { |
| char* error_message = string_piece_to_pool_string( |
| cf->pool, StringPiece("Invalid pagespeed gzip setting")); |
| return error_message; |
| } |
| return NGX_CONF_OK; |
| } |
| |
| // Some options require the worker process to be able to read and write to |
| // a specific directory. Generally the master process is root while the |
| // worker is nobody, so we need to change permissions and create the directory |
| // if necessary. |
| if (n_args == 2 && |
| (net_instaweb::StringCaseEqual("LogDir", args[0]) || |
| net_instaweb::StringCaseEqual("FileCachePath", args[0]))) { |
| char* error_message = ps_init_dir(args[0], args[1], cf); |
| if (error_message != NULL) { |
| return error_message; |
| } |
| // The directory has been prepared, but we haven't actually parsed the |
| // directive yet. That happens below in ParseAndSetOptions(). |
| } |
| |
| if (*options == NULL) { |
| *options = new NgxRewriteOptions( |
| cfg_m->driver_factory->thread_system()); |
| } |
| |
| bool process_script_variables = dynamic_cast<NgxRewriteDriverFactory*>( |
| cfg_m->driver_factory)->process_script_variables(); |
| |
| if (process_script_variables) { |
| // To be able to use '$', we map '$ps_dollar' to '$' via a script variable. |
| ngx_str_t name = ngx_string("ps_dollar"); |
| ngx_http_variable_t* var = ngx_http_add_variable( |
| cf, &name, NGX_HTTP_VAR_CHANGEABLE); |
| |
| if (var == NULL) { |
| return const_cast<char*>( |
| "Failed to add global configuration variable for '$ps_dollar'"); |
| } |
| var->get_handler = ps_dollar; |
| } |
| |
| const char* status = (*options)->ParseAndSetOptions( |
| args, n_args, cf->pool, handler, cfg_m->driver_factory, option_scope, cf, |
| process_script_variables); |
| |
| // nginx expects us to return a string literal but doesn't mark it const. |
| return const_cast<char*>(status); |
| } |
| |
| char* ps_main_configure(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { |
| ps_srv_conf_t* cfg_s = static_cast<ps_srv_conf_t*>( |
| ngx_http_conf_get_module_srv_conf(cf, ngx_pagespeed)); |
| return ps_configure(cf, &cfg_s->options, cfg_s->handler, |
| net_instaweb::RewriteOptions::kProcessScopeStrict); |
| } |
| |
| char* ps_srv_configure(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { |
| ps_srv_conf_t* cfg_s = static_cast<ps_srv_conf_t*>( |
| ngx_http_conf_get_module_srv_conf(cf, ngx_pagespeed)); |
| return ps_configure(cf, &cfg_s->options, cfg_s->handler, |
| net_instaweb::RewriteOptions::kServerScope); |
| } |
| |
| char* ps_loc_configure(ngx_conf_t* cf, ngx_command_t* cmd, void* conf) { |
| ps_loc_conf_t* cfg_l = static_cast<ps_loc_conf_t*>( |
| ngx_http_conf_get_module_loc_conf(cf, ngx_pagespeed)); |
| return ps_configure(cf, &cfg_l->options, cfg_l->handler, |
| net_instaweb::RewriteOptions::kDirectoryScope); |
| } |
| |
| void ps_cleanup_loc_conf(void* data) { |
| ps_loc_conf_t* cfg_l = static_cast<ps_loc_conf_t*>(data); |
| delete cfg_l->handler; |
| cfg_l->handler = NULL; |
| delete cfg_l->options; |
| cfg_l->options = NULL; |
| } |
| |
| bool factory_deleted = false; |
| void ps_cleanup_srv_conf(void* data) { |
| ps_srv_conf_t* cfg_s = static_cast<ps_srv_conf_t*>(data); |
| |
| // destroy the factory on the first call, causing all worker threads |
| // to be shut down when we destroy any proxy_fetch_factories. This |
| // will prevent any queued callbacks to destroyed proxy fetch factories |
| // from being executed |
| |
| if (!factory_deleted && cfg_s->server_context != NULL) { |
| NgxRewriteDriverFactory* factory = dynamic_cast<NgxRewriteDriverFactory*>( |
| cfg_s->server_context->factory()); |
| |
| if (!factory_init_called) { |
| factory->LoggingInit(ngx_cycle->log); |
| factory->Init(); |
| factory_init_called = true; |
| } |
| |
| delete factory; |
| factory_deleted = true; |
| } |
| if (cfg_s->proxy_fetch_factory != NULL) { |
| delete cfg_s->proxy_fetch_factory; |
| cfg_s->proxy_fetch_factory = NULL; |
| } |
| delete cfg_s->handler; |
| cfg_s->handler = NULL; |
| delete cfg_s->options; |
| cfg_s->options = NULL; |
| } |
| |
| void ps_cleanup_main_conf(void* data) { |
| ps_main_conf_t* cfg_m = static_cast<ps_main_conf_t*>(data); |
| delete cfg_m->handler; |
| cfg_m->handler = NULL; |
| NgxRewriteDriverFactory::Terminate(); |
| NgxRewriteOptions::Terminate(); |
| |
| // reset the factory deleted flag, so we will clean up properly next time, |
| // in case of a configuration reload. |
| // TODO(oschaaf): get rid of the factory_deleted flag |
| factory_deleted = false; |
| } |
| |
| template <typename ConfT> ConfT* ps_create_conf(ngx_conf_t* cf) { |
| ConfT* cfg = static_cast<ConfT*>(ngx_pcalloc(cf->pool, sizeof(ConfT))); |
| if (cfg == NULL) { |
| return NULL; |
| } |
| cfg->handler = new GoogleMessageHandler(); |
| return cfg; |
| } |
| |
| void ps_set_conf_cleanup_handler( |
| ngx_conf_t* cf, void (func)(void*), void* data) { // NOLINT |
| ngx_pool_cleanup_t* cleanup_m = ngx_pool_cleanup_add(cf->pool, 0); |
| if (cleanup_m == NULL) { |
| ngx_conf_log_error( |
| NGX_LOG_ERR, cf, 0, "failed to register a cleanup handler"); |
| } else { |
| cleanup_m->handler = func; |
| cleanup_m->data = data; |
| } |
| } |
| |
| void terminate_process_context() { |
| delete process_context; |
| process_context = NULL; |
| } |
| |
| void* ps_create_main_conf(ngx_conf_t* cf) { |
| if (!process_context_cleanup_hooked) { |
| atexit(terminate_process_context); |
| process_context_cleanup_hooked = true; |
| } |
| ps_main_conf_t* cfg_m = ps_create_conf<ps_main_conf_t>(cf); |
| if (cfg_m == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| CHECK(!factory_deleted); |
| NgxRewriteOptions::Initialize(); |
| NgxRewriteDriverFactory::Initialize(); |
| |
| cfg_m->driver_factory = new NgxRewriteDriverFactory( |
| *process_context, |
| new SystemThreadSystem(), |
| "" /* hostname, not used */, |
| -1 /* port, not used */); |
| factory_init_called = false; |
| ps_set_conf_cleanup_handler(cf, ps_cleanup_main_conf, cfg_m); |
| return cfg_m; |
| } |
| |
| void* ps_create_srv_conf(ngx_conf_t* cf) { |
| ps_srv_conf_t* cfg_s = ps_create_conf<ps_srv_conf_t>(cf); |
| if (cfg_s == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| ps_set_conf_cleanup_handler(cf, ps_cleanup_srv_conf, cfg_s); |
| return cfg_s; |
| } |
| |
| void* ps_create_loc_conf(ngx_conf_t* cf) { |
| ps_loc_conf_t* cfg_l = ps_create_conf<ps_loc_conf_t>(cf); |
| if (cfg_l == NULL) { |
| return NGX_CONF_ERROR; |
| } |
| ps_set_conf_cleanup_handler(cf, ps_cleanup_loc_conf, cfg_l); |
| return cfg_l; |
| } |
| |
| // nginx has hierarchical configuration. It maintains configurations at many |
| // levels. At various points it needs to merge configurations from different |
| // levels, and then it calls this. First it creates the configuration at the |
| // new level, parsing any pagespeed directives, then it merges in the |
| // configuration from the level above. This function should merge the parent |
| // configuration into the child. It's more complex than options->Merge() both |
| // because of the cases where the parent or child didn't have any pagespeed |
| // directives and because merging is order-dependent in the opposite way we'd |
| // like. |
| void ps_merge_options(NgxRewriteOptions* parent_options, |
| NgxRewriteOptions** child_options) { |
| if (parent_options == NULL) { |
| // Nothing to do. |
| } else if (*child_options == NULL) { |
| *child_options = parent_options->Clone(); |
| } else { // Both non-null. |
| // Unfortunately, merging configuration options is order dependent. We'd |
| // like to just do (*child_options)->Merge(*parent_options) |
| // but then if we had: |
| // pagespeed RewriteLevel PassThrough |
| // server { |
| // pagespeed RewriteLevel CoreFilters |
| // } |
| // it would always be stuck on PassThrough. |
| NgxRewriteOptions* child_specific_options = *child_options; |
| *child_options = parent_options->Clone(); |
| (*child_options)->Merge(*child_specific_options); |
| |
| if (child_specific_options->clear_inherited_scripts()) { |
| // We don't want to inherit any inherited script lines from the parent |
| // options here, so we just stick to the child specific ones. |
| child_specific_options->CopyScriptLinesTo(*child_options); |
| } else { |
| // We append the child specific script lines to the parent's script lines |
| // so we preserve the order in which they will be executed at request time |
| child_specific_options->AppendScriptLinesTo(*child_options); |
| } |
| delete child_specific_options; |
| } |
| } |
| |
| namespace { |
| |
| int times_ps_merge_srv_conf_called = 0; |
| |
| } // namespace |
| |
| // Called exactly once per server block to merge the main configuration with the |
| // configuration for this server. |
| char* ps_merge_srv_conf(ngx_conf_t* cf, void* parent, void* child) { |
| times_ps_merge_srv_conf_called += 1; |
| |
| ps_srv_conf_t* parent_cfg_s = static_cast<ps_srv_conf_t*>(parent); |
| ps_srv_conf_t* cfg_s = static_cast<ps_srv_conf_t*>(child); |
| |
| ps_merge_options(parent_cfg_s->options, &cfg_s->options); |
| |
| if (cfg_s->options == NULL) { |
| return NGX_CONF_OK; // No pagespeed options; don't do anything. |
| } |
| |
| // ServerContext needs a hostname and port, but I don't see how to get this |
| // and it ignores that a server can have multiple names and ports. Because |
| // the server context only needs them to make a unique identifier and to make |
| // debugging easier, substitute our own unique identifier. |
| // TODO(jefftk): either figure out how to get a hostname and port for this |
| // server block or change ServerContext not to ask for them. |
| int dummy_port = -times_ps_merge_srv_conf_called; |
| |
| ps_main_conf_t* cfg_m = static_cast<ps_main_conf_t*>( |
| ngx_http_conf_get_module_main_conf(cf, ngx_pagespeed)); |
| cfg_m->driver_factory->set_main_conf(parent_cfg_s->options); |
| cfg_s->server_context = cfg_m->driver_factory->MakeNgxServerContext( |
| "dummy_hostname", dummy_port); |
| // The server context sets some options when we call global_options(). So |
| // let it do that, then merge in options we got from the config file. |
| // Once we do that we're done with cfg_s->options. |
| cfg_s->server_context->global_options()->Merge(*cfg_s->options); |
| NgxRewriteOptions* ngx_options = dynamic_cast<NgxRewriteOptions*>( |
| cfg_s->server_context->global_options()); |
| cfg_s->options->CopyScriptLinesTo(ngx_options); |
| delete cfg_s->options; |
| cfg_s->options = NULL; |
| |
| if (cfg_s->server_context->global_options()->enabled()) { |
| // Validate FileCachePath |
| GoogleMessageHandler handler; |
| const char* file_cache_path = |
| cfg_s->server_context->config()->file_cache_path().c_str(); |
| if (file_cache_path[0] == '\0') { |
| return const_cast<char*>("FileCachePath must be set"); |
| } else if (!cfg_m->driver_factory->file_system()->IsDir( |
| file_cache_path, &handler).is_true()) { |
| return const_cast<char*>( |
| "FileCachePath must be an nginx-writeable directory"); |
| } |
| } |
| |
| return NGX_CONF_OK; |
| } |
| |
| char* ps_merge_loc_conf(ngx_conf_t* cf, void* parent, void* child) { |
| ps_loc_conf_t* cfg_l = static_cast<ps_loc_conf_t*>(child); |
| if (cfg_l->options == NULL) { |
| // No directory specific options. |
| return NGX_CONF_OK; |
| } |
| |
| // While you can't put a "location" block inside a "location" block you can |
| // put an "if" block inside a "location" block, which is implemented by making |
| // a pretend "location" block. In this case we may have pagespeed options |
| // from the parent "location" block as well as from the current locationish |
| // "if" block. |
| ps_loc_conf_t* parent_cfg_l = static_cast<ps_loc_conf_t*>(parent); |
| if (parent_cfg_l->options != NULL) { |
| // Rebase our options off of the ones defined in the parent location block. |
| ps_merge_options(parent_cfg_l->options, &cfg_l->options); |
| return NGX_CONF_OK; |
| } |
| |
| // Pagespeed options are defined in this location block, and it either has no |
| // parent (typical case) or is an if block whose parent location block defines |
| // no pagespeed options. Base our options off of those in the server block. |
| |
| ps_srv_conf_t* cfg_s = static_cast<ps_srv_conf_t*>( |
| ngx_http_conf_get_module_srv_conf(cf, ngx_pagespeed)); |
| |
| if (cfg_s->server_context == NULL) { |
| // Pagespeed options cannot be defined only in location blocks. There must |
| // be at least a single "pagespeed off" in the main block or a server |
| // block. |
| return NGX_CONF_OK; |
| } |
| |
| // If we get here we have parent options ("global options") from cfg_s, child |
| // options ("directory specific options") from cfg_l, and no options from |
| // parent_cfg_l. Rebase the directory specific options on the global options. |
| ps_merge_options(cfg_s->server_context->config(), &cfg_l->options); |
| |
| return NGX_CONF_OK; |
| } |
| |
| // _ef_ is a shorthand for ETag Filter |
| ngx_http_output_header_filter_pt ngx_http_ef_next_header_filter; |
| |
| // Tell nginx whether we have network activity we're waiting for so that it sets |
| // a write handler. See src/http/ngx_http_request.c:2083. |
| void ps_set_buffered(ngx_http_request_t* r, bool on) { |
| if (on) { |
| r->buffered |= NGX_HTTP_PAGESPEED_BUFFERED; |
| } else { |
| r->buffered &= ~NGX_HTTP_PAGESPEED_BUFFERED; |
| } |
| } |
| |
| bool ps_is_https(ngx_http_request_t* r) { |
| // Based on ngx_http_variable_scheme. |
| #if (NGX_HTTP_SSL) |
| return r->connection->ssl; |
| #endif |
| return false; |
| } |
| |
| int ps_determine_port(ngx_http_request_t* r) { |
| // Return -1 if the port isn't specified, the port number otherwise. |
| // |
| // If a Host header was provided, get the host from that. Otherwise fall back |
| // to the local port of the incoming connection. |
| |
| int port = -1; |
| ngx_table_elt_t* host = r->headers_in.host; |
| |
| if (host != NULL) { |
| // Host headers can look like: |
| // |
| // www.example.com // normal |
| // www.example.com:8080 // port specified |
| // 127.0.0.1 // IPv4 |
| // 127.0.0.1:8080 // IPv4 with port |
| // [::1] // IPv6 |
| // [::1]:8080 // IPv6 with port |
| // |
| // The IPv6 ones are the annoying ones, but the square brackets allow us to |
| // disambiguate. To find the port number, we can say: |
| // |
| // 1) Take the text after the final colon. |
| // 2) If all of those characters are digits, that's your port number |
| // |
| // In the case of a plain IPv6 address with no port number, the text after |
| // the final colon will include a ']', so we'll stop processing. |
| |
| StringPiece host_str = str_to_string_piece(host->value); |
| size_t colon_index = host_str.rfind(":"); |
| if (colon_index == host_str.npos) { |
| return -1; |
| } |
| // Strip everything up to and including the final colon. |
| host_str.remove_prefix(colon_index + 1); |
| |
| bool ok = StringToInt(host_str, &port); |
| if (!ok) { |
| // Might be malformed port, or just IPv6 with no port specified. |
| return -1; |
| } |
| |
| return port; |
| } |
| |
| // Based on ngx_http_variable_server_port. |
| #if (NGX_HAVE_INET6) |
| if (r->connection->local_sockaddr->sa_family == AF_INET6) { |
| port = ntohs(reinterpret_cast<struct sockaddr_in6*>( |
| r->connection->local_sockaddr)->sin6_port); |
| } |
| #endif |
| if (port == -1 /* still need port */) { |
| port = ntohs(reinterpret_cast<struct sockaddr_in*>( |
| r->connection->local_sockaddr)->sin_port); |
| } |
| |
| return port; |
| } |
| } // namespace |
| |
| StringPiece ps_determine_host(ngx_http_request_t* r) { |
| StringPiece host = str_to_string_piece(r->headers_in.server); |
| if (host.size() == 0) { |
| // If host is unspecified, perhaps because of a pure HTTP 1.0 "GET /path", |
| // fall back to server IP address. Based on ngx_http_variable_server_addr. |
| ngx_str_t s; |
| u_char addr[NGX_SOCKADDR_STRLEN]; |
| s.len = NGX_SOCKADDR_STRLEN; |
| s.data = addr; |
| ngx_int_t rc = ngx_connection_local_sockaddr(r->connection, &s, 0); |
| if (rc != NGX_OK) { |
| s.len = 0; |
| } |
| host = str_to_string_piece(s); |
| } |
| return host; |
| } |
| |
| namespace { |
| |
| GoogleString ps_determine_url(ngx_http_request_t* r) { |
| int port = ps_determine_port(r); |
| GoogleString port_string; |
| if ((ps_is_https(r) && (port == 443 || port == -1)) || |
| (!ps_is_https(r) && (port == 80 || port == -1))) { |
| // No port specifier needed for requests on default ports. |
| port_string = ""; |
| } else { |
| port_string = StrCat(":", IntegerToString(port)); |
| } |
| |
| StringPiece host = ps_determine_host(r); |
| |
| return StrCat(ps_is_https(r) ? "https://" : "http://", |
| host, port_string, str_to_string_piece(r->unparsed_uri)); |
| } |
| |
| // Get the context for this request. ps_connection_read_handler should already |
| // have been called to create it. |
| ps_request_ctx_t* ps_get_request_context(ngx_http_request_t* r) { |
| return static_cast<ps_request_ctx_t*>( |
| ngx_http_get_module_ctx(r, ngx_pagespeed)); |
| } |
| |
| void ps_release_base_fetch(ps_request_ctx_t* ctx); |
| |
| // we are still at pagespeed phase |
| ngx_int_t ps_decline_request(ngx_http_request_t* r) { |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| CHECK(ctx != NULL); |
| |
| // re init ctx |
| ctx->fetch_done = false; |
| ctx->write_pending = false; |
| |
| ps_set_buffered(r, false); |
| |
| r->count++; |
| r->phase_handler++; |
| r->write_event_handler = ngx_http_core_run_phases; |
| ngx_http_core_run_phases(r); |
| ngx_http_run_posted_requests(r->connection); |
| return NGX_DONE; |
| } |
| |
| ngx_int_t ps_async_wait_response(ngx_http_request_t* r) { |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| CHECK(ctx != NULL); |
| |
| r->count++; |
| r->write_event_handler = ngx_http_request_empty_handler; |
| ps_set_buffered(r, true); |
| // We don't need to add a timer here, as it will be set by nginx. |
| return NGX_DONE; |
| } |
| |
| namespace { |
| |
| ngx_http_output_header_filter_pt ngx_http_next_header_filter; |
| ngx_http_output_body_filter_pt ngx_http_next_body_filter; |
| |
| ngx_int_t ps_base_fetch_filter(ngx_http_request_t* r, ngx_chain_t* in) { |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| |
| if (ctx == NULL || ctx->base_fetch == NULL) { |
| return ngx_http_next_body_filter(r, in); |
| } |
| |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "http pagespeed write filter \"%V\"", &r->uri); |
| |
| // send response body |
| if (in || ctx->write_pending) { |
| ngx_int_t rc = ngx_http_next_body_filter(r, in); |
| ctx->write_pending = (rc == NGX_AGAIN); |
| if (rc == NGX_OK && !ctx->fetch_done) { |
| return NGX_AGAIN; |
| } |
| return rc; |
| } |
| |
| return ctx->fetch_done ? NGX_OK : NGX_AGAIN; |
| } |
| |
| // This runs on the nginx event loop in response to seeing the byte PageSpeed |
| // sent over the pipe to trigger the nginx-side code. Copy whatever is ready |
| // from PageSpeed out to the browser (headers and/or body). |
| ngx_int_t ps_base_fetch_handler(ngx_http_request_t* r) { |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| ngx_int_t rc; |
| ngx_chain_t* cl = NULL; |
| |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "ps fetch handler: %V", &r->uri); |
| |
| if (!r->header_sent) { |
| if (ctx->preserve_caching_headers != kDontPreserveHeaders) { |
| ngx_table_elt_t* header; |
| NgxListIterator it(&(r->headers_out.headers.part)); |
| while ((header = it.Next()) != NULL) { |
| // We need to remember a few headers when ModifyCachingHeaders is off, |
| // so we can send them unmodified in copy_response_headers_to_ngx(). |
| // This just sets the hash to 0 for all other headers. That way, we |
| // avoid some relatively complicated code to reconstruct these headers. |
| if (!(STR_CASE_EQ_LITERAL(header->key, "Cache-Control") || |
| (ctx->preserve_caching_headers == kPreserveAllCachingHeaders && |
| (STR_CASE_EQ_LITERAL(header->key, "Etag") || |
| STR_CASE_EQ_LITERAL(header->key, "Date") || |
| STR_CASE_EQ_LITERAL(header->key, "Last-Modified") || |
| STR_CASE_EQ_LITERAL(header->key, "Expires"))))) { |
| header->hash = 0; |
| if (STR_CASE_EQ_LITERAL(header->key, "Location")) { |
| // There's a possible issue with the location header, where setting |
| // the hash to 0 is not enough. See: |
| // https://github.com/nginx/nginx/blob/master/src/http/ngx_http_header_filter_module.c#L314 |
| r->headers_out.location = NULL; |
| } |
| } |
| } |
| } else { |
| ngx_http_clean_header(r); |
| } |
| // collect response headers from pagespeed |
| rc = ctx->base_fetch->CollectHeaders(&r->headers_out); |
| if (rc == NGX_ERROR) { |
| return NGX_HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| // send response headers |
| rc = ngx_http_next_header_filter(r); |
| |
| // standard nginx send header check see ngx_http_send_response |
| if (rc == NGX_ERROR || rc > NGX_OK) { |
| return ngx_http_filter_finalize_request(r, NULL, rc); |
| } |
| |
| // for in_place_check_header_filter |
| if (rc < NGX_OK && rc != NGX_AGAIN) { |
| CHECK(rc == NGX_DONE); |
| return rc; |
| } |
| |
| ctx->write_pending = (rc == NGX_AGAIN); |
| |
| ps_set_buffered(r, true); |
| } |
| |
| // collect response body from pagespeed |
| // Pass the optimized content along to later body filters. |
| // From Weibin: This function should be called mutiple times. Store the |
| // whole file in one chain buffers is too aggressive. It could consume |
| // too much memory in busy servers. |
| |
| rc = ctx->base_fetch->CollectAccumulatedWrites(&cl); |
| ngx_log_error(NGX_LOG_DEBUG, ctx->r->connection->log, 0, |
| "CollectAccumulatedWrites, %d", rc); |
| |
| if (rc == NGX_ERROR) { |
| ps_set_buffered(r, false); |
| return NGX_HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| if (rc == NGX_AGAIN && cl == NULL) { |
| // there is no body buffer to send now. |
| return NGX_AGAIN; |
| } |
| |
| if (rc == NGX_OK) { |
| ps_set_buffered(r, false); |
| ctx->fetch_done = true; |
| } |
| |
| return ps_base_fetch_filter(r, cl); |
| } |
| |
| void ps_base_fetch_filter_init() { |
| ngx_http_next_header_filter = ngx_http_top_header_filter; |
| ngx_http_next_body_filter = ngx_http_top_body_filter; |
| ngx_http_top_body_filter = ps_base_fetch_filter; |
| } |
| |
| } // namespace |
| |
| // Do some bookkeeping, cleanup, and error checking to keep the mess out of |
| // ps_base_fetch_handler. |
| void ps_connection_read_handler(ngx_event_t* ev) { |
| CHECK(ev != NULL); |
| ngx_connection_t* c = static_cast<ngx_connection_t*>(ev->data); |
| CHECK(c != NULL); |
| |
| int rc; |
| |
| // Request has been finalized, do nothing just clear the pipe. |
| if (c->error) { |
| do { |
| char chr[256]; |
| rc = read(c->fd, chr, 256); |
| } while (rc > 0 || (rc == -1 && errno == EINTR)); // Retry on EINTR. |
| |
| if (rc == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { |
| return; |
| } |
| |
| // Write peer close or error occur. |
| ngx_close_connection(c); |
| return; |
| } |
| |
| ps_request_ctx_t* ctx = static_cast<ps_request_ctx_t*>(c->data); |
| CHECK(ctx != NULL); |
| ngx_http_request_t* r = ctx->r; |
| CHECK(r != NULL); |
| |
| // Clear the pipe. |
| do { |
| char chr[256]; |
| rc = read(c->fd, chr, 256); |
| } while (rc > 0 || (rc == -1 && errno == EINTR)); // Retry on EINTR. |
| |
| if (r->connection->error) { |
| ngx_log_error(NGX_LOG_DEBUG, ngx_cycle->log, 0, |
| "pagespeed [%p] request already finalized", r); |
| ctx->pagespeed_connection = NULL; |
| ngx_close_connection(c); |
| ngx_http_finalize_request(r, NGX_ERROR); |
| return; |
| } |
| |
| if (rc == -1 && errno != EAGAIN && errno != EWOULDBLOCK) { |
| ctx->pagespeed_connection = NULL; |
| ngx_close_connection(c); |
| return ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| } |
| |
| // AGAIN or rc == 0. |
| if (rc == 0) { |
| // Close the pipe here to avoid SIGPIPE |
| // Done will be check in RequestCollection. |
| ctx->pagespeed_connection = NULL; |
| ngx_close_connection(c); |
| } |
| |
| if (ctx->fetch_done) { |
| return; |
| } |
| |
| ngx_http_finalize_request(r, ps_base_fetch_handler(r)); |
| } |
| |
| ngx_int_t ps_create_connection( |
| ps_request_ctx_t* ctx, NgxServerContext* server_context, int pipe_fd) { |
| // We have to use the server_context's log (which is the server context's |
| // ngx_http_core_loc_conf_t->error_log) and not the request's log because |
| // this connection can outlast the request by a little while. |
| ngx_log_t* server_context_log = server_context->ngx_message_handler()->log(); |
| if (server_context_log == NULL) { |
| ngx_log_debug0(NGX_LOG_INFO, ctx->r->connection->log, 0, |
| "ps_create_connection failed to get server context log"); |
| return NGX_ERROR; |
| } |
| |
| ngx_connection_t* c = ngx_get_connection(pipe_fd, server_context_log); |
| if (c == NULL) { |
| return NGX_ERROR; |
| } |
| |
| c->recv = ngx_recv; |
| c->send = ngx_send; |
| c->recv_chain = ngx_recv_chain; |
| c->send_chain = ngx_send_chain; |
| |
| c->log_error = ctx->r->connection->log_error; |
| |
| c->read->log = c->log; |
| c->write->log = c->log; |
| |
| ctx->pagespeed_connection = c; |
| |
| // Tell nginx to monitor this pipe and call us back when there's data. |
| c->data = ctx; |
| c->read->handler = ps_connection_read_handler; |
| ngx_add_event(c->read, NGX_READ_EVENT, 0); |
| |
| return NGX_OK; |
| } |
| |
| // Populate cfg_* with configuration information for this request. |
| // Thin wrappers around ngx_http_get_module_*_conf and cast. |
| ps_srv_conf_t* ps_get_srv_config(ngx_http_request_t* r) { |
| return static_cast<ps_srv_conf_t*>( |
| ngx_http_get_module_srv_conf(r, ngx_pagespeed)); |
| } |
| ps_loc_conf_t* ps_get_loc_config(ngx_http_request_t* r) { |
| return static_cast<ps_loc_conf_t*>( |
| ngx_http_get_module_loc_conf(r, ngx_pagespeed)); |
| } |
| |
| // Wrapper around GetQueryOptions() |
| RewriteOptions* ps_determine_request_options( |
| ngx_http_request_t* r, |
| const RewriteOptions* domain_options, /* may be null */ |
| RequestHeaders* request_headers, |
| ResponseHeaders* response_headers, |
| RequestContextPtr request_context, |
| ps_srv_conf_t* cfg_s, |
| GoogleUrl* url, |
| GoogleString* pagespeed_query_params, |
| GoogleString* pagespeed_option_cookies) { |
| // Sets option from request headers and url. |
| RewriteQuery rewrite_query; |
| if (!cfg_s->server_context->GetQueryOptions( |
| request_context, domain_options, url, request_headers, |
| response_headers, &rewrite_query)) { |
| // Failed to parse query params or request headers. Treat this as if there |
| // were no query params given. |
| ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| "ps_route request: parsing headers or query params failed."); |
| return NULL; |
| } |
| |
| *pagespeed_query_params = |
| rewrite_query.pagespeed_query_params().ToEscapedString(); |
| *pagespeed_option_cookies = |
| rewrite_query.pagespeed_option_cookies().ToEscapedString(); |
| |
| // Will be NULL if there aren't any options set with query params or in |
| // headers. |
| return rewrite_query.ReleaseOptions(); |
| } |
| |
| // Check whether this visitor is already in an experiment. If they're not, |
| // classify them into one by setting a cookie. Then set options appropriately |
| // for their experiment. |
| // |
| // See InstawebContext::SetExperimentStateAndCookie() |
| bool ps_set_experiment_state_and_cookie(ngx_http_request_t* r, |
| RequestHeaders* request_headers, |
| RewriteOptions* options, |
| const StringPiece& host) { |
| CHECK(options->running_experiment()); |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| bool need_cookie = cfg_s->server_context->experiment_matcher()-> |
| ClassifyIntoExperiment(*request_headers, options); |
| if (need_cookie && host.length() > 0) { |
| PosixTimer timer; |
| int64 time_now_ms = timer.NowMs(); |
| int64 expiration_time_ms = (time_now_ms + |
| options->experiment_cookie_duration_ms()); |
| |
| // TODO(jefftk): refactor SetExperimentCookie to expose the value we want to |
| // set on the cookie. |
| int state = options->experiment_id(); |
| GoogleString expires; |
| ConvertTimeToString(expiration_time_ms, &expires); |
| GoogleString value = StringPrintf( |
| "%s=%s; Expires=%s; Domain=.%s; Path=/", |
| experiment::kExperimentCookie, |
| experiment::ExperimentStateToCookieString(state).c_str(), |
| expires.c_str(), host.as_string().c_str()); |
| |
| // Set the PagespeedExperiment cookie. |
| ngx_table_elt_t* cookie = static_cast<ngx_table_elt_t*>( |
| ngx_list_push(&r->headers_out.headers)); |
| if (cookie == NULL) { |
| return false; |
| } |
| cookie->hash = 1; // Include this header in the response. |
| |
| ngx_str_set(&cookie->key, "Set-Cookie"); |
| // It's not safe to use value.c_str here because cookie header only keeps a |
| // pointer to the string data. |
| cookie->value.data = reinterpret_cast<u_char*>( |
| string_piece_to_pool_string(r->pool, value)); |
| cookie->value.len = value.size(); |
| } |
| return true; |
| } |
| |
| // There are many sources of options: |
| // - the request (query parameters, headers, and cookies) |
| // - location block |
| // - global server options |
| // - experiment framework |
| // Consider them all, returning appropriate options for this request, of which |
| // the caller takes ownership. If the only applicable options are global, |
| // set options to NULL so we can use server_context->global_options(). |
| bool ps_determine_options(ngx_http_request_t* r, |
| RequestHeaders* request_headers, |
| ResponseHeaders* response_headers, |
| RewriteOptions** options, |
| RequestContextPtr request_context, |
| ps_srv_conf_t* cfg_s, |
| GoogleUrl* url, |
| GoogleString* pagespeed_query_params, |
| GoogleString* pagespeed_option_cookies, |
| bool html_rewrite) { |
| ps_loc_conf_t* cfg_l = ps_get_loc_config(r); |
| |
| // Global options for this server. Never null. |
| RewriteOptions* global_options = cfg_s->server_context->global_options(); |
| |
| // Directory-specific options, usually null. They've already been rebased off |
| // of the global options as part of the configuration process. |
| RewriteOptions* directory_options = cfg_l->options; |
| |
| // Request-specific options, nearly always null. If set they need to be |
| // rebased on the directory options or the global options. |
| RewriteOptions* request_options = ps_determine_request_options( |
| r, directory_options, request_headers, response_headers, request_context, |
| cfg_s, url, pagespeed_query_params, pagespeed_option_cookies); |
| bool have_request_options = request_options != NULL; |
| |
| // Because the caller takes ownership of any options we return, the only |
| // situation in which we can avoid allocating a new RewriteOptions is if the |
| // global options are ok as they are and we don't have script variables we |
| // need to evaluate at this point. |
| NgxRewriteOptions* ngx_global_options = |
| dynamic_cast<NgxRewriteOptions*>(global_options); |
| if (!have_request_options && directory_options == NULL && |
| !global_options->running_experiment() && |
| ngx_global_options->script_lines().size() == 0) { |
| return true; |
| } |
| |
| // Start with directory options if we have them, otherwise request options. |
| if (directory_options != NULL) { |
| *options = directory_options->Clone(); |
| } else { |
| *options = global_options->Clone(); |
| } |
| |
| NgxRewriteDriverFactory* ngx_factory = dynamic_cast<NgxRewriteDriverFactory*>( |
| cfg_s->server_context->factory()); |
| NgxRewriteOptions* ngx_options = dynamic_cast<NgxRewriteOptions*>(*options); |
| |
| // ExecuteScriptVariables() sets 'pagespeed off' on ngx_options when execution |
| // fails and then returns false. When that happens we return, as we don't want |
| // to allow enabling pagespeed by request and execute without the intended |
| // configuration. |
| if (!ngx_options->ExecuteScriptVariables(r, cfg_s->handler, ngx_factory)) { |
| return false; |
| } |
| |
| // Modify our options in response to request options if specified. |
| if (have_request_options) { |
| (*options)->Merge(*request_options); |
| delete request_options; |
| request_options = NULL; |
| } |
| |
| // If we're running an experiment and processing html then modify our options |
| // in response to the experiment. Except we generally don't want experiments |
| // to be contaminated with unexpected settings, so ignore experiments if we |
| // have request-specific options. Unless EnrollExperiment is on, probably set |
| // by a query parameter, in which case we want to go ahead and apply the |
| // experimental settings even if it means bad data, because we're just seeing |
| // what it looks like. |
| if ((*options)->running_experiment() && |
| html_rewrite && |
| (!have_request_options || |
| (*options)->enroll_experiment())) { |
| bool ok = ps_set_experiment_state_and_cookie( |
| r, request_headers, *options, url->Host()); |
| if (!ok) { |
| delete *options; |
| *options = NULL; |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| // Fix URL based on X-Forwarded-Proto. |
| // http://code.google.com/p/modpagespeed/issues/detail?id=546 For example, if |
| // Apache gives us the URL "http://www.example.com/" and there is a header: |
| // "X-Forwarded-Proto: https", then we update this base URL to |
| // "https://www.example.com/". This only ever changes the protocol of the url. |
| // |
| // Returns true if it modified url, false otherwise. |
| bool ps_apply_x_forwarded_proto(ngx_http_request_t* r, GoogleString* url) { |
| // First check for an X-Forwarded-Proto header. |
| const ngx_str_t* x_forwarded_proto_header = NULL; |
| |
| ngx_table_elt_t* header; |
| NgxListIterator it(&(r->headers_in.headers.part)); |
| while ((header = it.Next()) != NULL) { |
| if (STR_CASE_EQ_LITERAL(header->key, "X-Forwarded-Proto")) { |
| x_forwarded_proto_header = &header->value; |
| break; |
| } |
| } |
| |
| if (x_forwarded_proto_header == NULL) { |
| return false; // No X-Forwarded-Proto header found. |
| } |
| |
| StringPiece x_forwarded_proto = |
| str_to_string_piece(*x_forwarded_proto_header); |
| if (!STR_CASE_EQ_LITERAL(*x_forwarded_proto_header, "http") && |
| !STR_CASE_EQ_LITERAL(*x_forwarded_proto_header, "https")) { |
| LOG(WARNING) << "Unsupported X-Forwarded-Proto: " << x_forwarded_proto |
| << " for URL " << url << " protocol not changed."; |
| return false; |
| } |
| |
| StringPiece url_sp(*url); |
| StringPiece::size_type colon_pos = url_sp.find(":"); |
| |
| if (colon_pos == StringPiece::npos) { |
| return false; // URL appears to have no protocol; give up. |
| } |
| |
| // Replace URL protocol with that specified in X-Forwarded-Proto. |
| *url = StrCat(x_forwarded_proto, url_sp.substr(colon_pos)); |
| |
| return true; |
| } |
| |
| bool is_pagespeed_subrequest(ngx_http_request_t* r) { |
| ngx_table_elt_t* user_agent_header = r->headers_in.user_agent; |
| if (user_agent_header == NULL) { |
| return false; |
| } |
| StringPiece user_agent = str_to_string_piece(user_agent_header->value); |
| return (user_agent.find(kModPagespeedSubrequestUserAgent) != user_agent.npos); |
| } |
| |
| // TODO(chaizhenhua): merge into NgxBaseFetch::Release() |
| void ps_release_base_fetch(ps_request_ctx_t* ctx) { |
| // In the normal flow BaseFetch doesn't delete itself in HandleDone() because |
| // we still need to receive notification via pipe and call |
| // CollectAccumulatedWrites. If there's an error and we're cleaning up early |
| // then HandleDone() hasn't been called yet and we need the base fetch to wait |
| // for that and then delete itself. |
| if (ctx->base_fetch != NULL) { |
| ctx->base_fetch->Release(); |
| ctx->base_fetch = NULL; |
| } |
| |
| if (ctx->pagespeed_connection != NULL) { |
| // Tell pagespeed connection ctx has been released. |
| ctx->pagespeed_connection->error = 1; |
| ctx->pagespeed_connection = NULL; |
| } |
| } |
| |
| // TODO(chaizhenhua): merge into NgxBaseFetch ctor |
| ngx_int_t ps_create_base_fetch(ps_request_ctx_t* ctx, |
| RequestContextPtr request_context, |
| RequestHeaders* request_headers) { |
| ngx_http_request_t* r = ctx->r; |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| int file_descriptors[2]; |
| |
| int rc = pipe(file_descriptors); |
| if (rc != 0) { |
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "pipe() failed"); |
| return NGX_ERROR; |
| } |
| |
| if (ngx_nonblocking(file_descriptors[0]) == -1) { |
| ngx_log_error(NGX_LOG_EMERG, r->connection->log, ngx_socket_errno, |
| ngx_nonblocking_n " pipe[0] failed"); |
| close(file_descriptors[0]); |
| close(file_descriptors[1]); |
| return NGX_ERROR; |
| } |
| |
| if (ngx_nonblocking(file_descriptors[1]) == -1) { |
| ngx_log_error(NGX_LOG_EMERG, r->connection->log, ngx_socket_errno, |
| ngx_nonblocking_n " pipe[1] failed"); |
| close(file_descriptors[0]); |
| close(file_descriptors[1]); |
| return NGX_ERROR; |
| } |
| |
| rc = ps_create_connection(ctx, cfg_s->server_context, file_descriptors[0]); |
| if (rc != NGX_OK) { |
| close(file_descriptors[0]); |
| close(file_descriptors[1]); |
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, |
| "ps_route_request: no pagespeed connection."); |
| return NGX_ERROR; |
| } |
| |
| // Handles its own deletion. We need to call Release() when we're done with |
| // it, and call Done() on the associated parent (Proxy or Resource) fetch. If |
| // we fail before creating the associated fetch then we need to call Done() on |
| // the BaseFetch ourselves. |
| ctx->base_fetch = new NgxBaseFetch( |
| r, file_descriptors[1], cfg_s->server_context, |
| request_context, ctx->preserve_caching_headers); |
| ctx->base_fetch->SetRequestHeadersTakingOwnership(request_headers); |
| |
| return NGX_OK; |
| } |
| |
| void ps_release_request_context(void* data) { |
| ps_request_ctx_t* ctx = static_cast<ps_request_ctx_t*>(data); |
| |
| // proxy_fetch deleted itself if we called Done(), but if an error happened |
| // before then we need to tell it to delete itself. |
| // |
| // If this is a resource fetch then proxy_fetch was never initialized. |
| if (ctx->proxy_fetch != NULL) { |
| ctx->proxy_fetch->Done(false /* failure */); |
| ctx->proxy_fetch = NULL; |
| } |
| |
| if (ctx->inflater_ != NULL) { |
| delete ctx->inflater_; |
| ctx->inflater_ = NULL; |
| } |
| |
| if (ctx->driver != NULL) { |
| ctx->driver->Cleanup(); |
| ctx->driver = NULL; |
| } |
| |
| if (ctx->recorder != NULL) { |
| ctx->recorder->Fail(); |
| ctx->recorder->DoneAndSetHeaders(NULL); // Deletes recorder. |
| ctx->recorder = NULL; |
| } |
| |
| ps_release_base_fetch(ctx); |
| delete ctx; |
| } |
| |
| // Set us up for processing a request. Creates a request context and determines |
| // which handler should deal with the request. |
| RequestRouting::Response ps_route_request(ngx_http_request_t* r, |
| bool is_resource_fetch) { |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| |
| if (!cfg_s->server_context->global_options()->enabled()) { |
| // Not enabled for this server block. |
| return RequestRouting::kPagespeedDisabled; |
| } |
| |
| if (r->err_status != 0) { |
| return RequestRouting::kErrorResponse; |
| } |
| |
| GoogleString url_string = ps_determine_url(r); |
| GoogleUrl url(url_string); |
| |
| if (!url.IsWebValid()) { |
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid url"); |
| |
| // Let nginx deal with the error however it wants; we will see a NULL ctx in |
| // the body filter or content handler and do nothing. |
| return RequestRouting::kInvalidUrl; |
| } |
| |
| if (is_pagespeed_subrequest(r)) { |
| return RequestRouting::kPagespeedSubrequest; |
| } else if ( |
| url.PathSansLeaf() == dynamic_cast<NgxRewriteDriverFactory*>( |
| cfg_s->server_context->factory())->static_asset_prefix()) { |
| return RequestRouting::kStaticContent; |
| } |
| |
| const NgxRewriteOptions* global_options = cfg_s->server_context->config(); |
| |
| StringPiece path = url.PathSansQuery(); |
| if (StringCaseEqual(path, global_options->statistics_path())) { |
| return RequestRouting::kStatistics; |
| } else if (StringCaseEqual(path, global_options->global_statistics_path())) { |
| return RequestRouting::kGlobalStatistics; |
| } else if (StringCaseEqual(path, global_options->console_path())) { |
| return RequestRouting::kConsole; |
| } else if (StringCaseEqual(path, global_options->messages_path())) { |
| return RequestRouting::kMessages; |
| } else if ( |
| // The admin handlers get everything under a path (/path/*) while all the |
| // other handlers only get exact matches (/path). So match all paths |
| // starting with the handler path. |
| !global_options->admin_path().empty() && |
| StringCaseStartsWith(path, global_options->admin_path())) { |
| return RequestRouting::kAdmin; |
| } else if (!global_options->global_admin_path().empty() && |
| StringCaseStartsWith(path, global_options->global_admin_path())) { |
| return RequestRouting::kGlobalAdmin; |
| } else if (global_options->enable_cache_purge() && |
| !global_options->purge_method().empty() && |
| (global_options->purge_method() == |
| str_to_string_piece(r->method_name))) { |
| return RequestRouting::kCachePurge; |
| } |
| |
| const GoogleString* beacon_url; |
| if (ps_is_https(r)) { |
| beacon_url = &(global_options->beacon_url().https); |
| } else { |
| beacon_url = &(global_options->beacon_url().http); |
| } |
| |
| if (url.PathSansQuery() == StringPiece(*beacon_url)) { |
| return RequestRouting::kBeacon; |
| } |
| |
| return RequestRouting::kResource; |
| } |
| |
| ngx_int_t ps_resource_handler(ngx_http_request_t* r, |
| bool html_rewrite, |
| RequestRouting::Response response_category) { |
| if (r != r->main) { |
| return NGX_DECLINED; |
| } |
| |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| |
| CHECK(!(html_rewrite && (ctx == NULL || ctx->html_rewrite == false))); |
| |
| if (!html_rewrite && |
| r->method != NGX_HTTP_GET && |
| r->method != NGX_HTTP_HEAD && |
| r->method != NGX_HTTP_POST && |
| response_category != RequestRouting::kCachePurge) { |
| return NGX_DECLINED; |
| } |
| |
| GoogleString url_string = ps_determine_url(r); |
| GoogleUrl url(url_string); |
| |
| if (!url.IsWebValid()) { |
| ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid url"); |
| return NGX_DECLINED; |
| } |
| |
| scoped_ptr<RequestHeaders> request_headers(new RequestHeaders); |
| scoped_ptr<ResponseHeaders> response_headers(new ResponseHeaders); |
| |
| copy_request_headers_from_ngx(r, request_headers.get()); |
| copy_response_headers_from_ngx(r, response_headers.get()); |
| |
| RequestContextPtr request_context( |
| cfg_s->server_context->NewRequestContext(r)); |
| RewriteOptions* options = NULL; |
| GoogleString pagespeed_query_params; |
| GoogleString pagespeed_option_cookies; |
| |
| if (!ps_determine_options(r, request_headers.get(), response_headers.get(), |
| &options, request_context, cfg_s, &url, |
| &pagespeed_query_params, &pagespeed_option_cookies, |
| html_rewrite)) { |
| return NGX_ERROR; |
| } |
| |
| // Take ownership of custom_options. |
| scoped_ptr<RewriteOptions> custom_options(options); |
| if (options == NULL) { |
| options = cfg_s->server_context->global_options(); |
| } |
| |
| if (!options->enabled()) { |
| // Disabled via query params or request headers. |
| return NGX_DECLINED; |
| } |
| |
| request_context->set_options(options->ComputeHttpOptions()); |
| |
| // ps_determine_options modified url, removing any ModPagespeedFoo=Bar query |
| // parameters. Keep url_string in sync with url. |
| url.Spec().CopyToString(&url_string); |
| |
| if (cfg_s->server_context->global_options()->respect_x_forwarded_proto()) { |
| bool modified_url = ps_apply_x_forwarded_proto(r, &url_string); |
| if (modified_url) { |
| url.Reset(url_string); |
| CHECK(url.IsWebValid()) << "The output of ps_apply_x_forwarded_proto" |
| << " should always be a valid url because it only" |
| << " changes the scheme between http and https."; |
| } |
| } |
| |
| bool pagespeed_resource = |
| !html_rewrite && cfg_s->server_context->IsPagespeedResource(url); |
| bool is_an_admin_handler = |
| response_category == RequestRouting::kStatistics || |
| response_category == RequestRouting::kGlobalStatistics || |
| response_category == RequestRouting::kConsole || |
| response_category == RequestRouting::kAdmin || |
| response_category == RequestRouting::kGlobalAdmin || |
| response_category == RequestRouting::kCachePurge; |
| |
| if (html_rewrite) { |
| ps_release_base_fetch(ctx); |
| } else { |
| // create request ctx |
| CHECK(ctx == NULL); |
| ctx = new ps_request_ctx_t(); |
| |
| ctx->base_fetch = NULL; |
| ctx->pagespeed_connection = NULL; |
| ctx->r = r; |
| ctx->html_rewrite = false; |
| ctx->in_place = false; |
| ctx->write_pending = false; |
| ctx->fetch_done = false; |
| ctx->preserve_caching_headers = kDontPreserveHeaders; |
| ctx->proxy_fetch = NULL; |
| ctx->inflater_ = NULL; |
| ctx->driver = NULL; |
| ctx->recorder = NULL; |
| ctx->ipro_response_headers = NULL; |
| |
| // See build_context_for_request() in mod_instaweb.cc |
| // TODO(jefftk): Is this the right place to be modifying caching headers for |
| // html fetches? Or should that be done later, in the headers flow for |
| // filter mode, rather than here in resource fetch mode? |
| if (!options->modify_caching_headers()) { |
| ctx->preserve_caching_headers = kPreserveAllCachingHeaders; |
| } else if (!options->IsDownstreamCacheIntegrationEnabled()) { |
| // Downstream cache integration is not enabled. Disable original |
| // Cache-Control headers. |
| ctx->preserve_caching_headers = kDontPreserveHeaders; |
| } else if (!pagespeed_resource && !is_an_admin_handler) { |
| ctx->preserve_caching_headers = kPreserveOnlyCacheControl; |
| // Downstream cache integration is enabled. If a rebeaconing key has been |
| // configured and there is a ShouldBeacon header with the correct key, |
| // disable original Cache-Control headers so that the instrumented page is |
| // served out with no-cache. |
| StringPiece should_beacon(request_headers->Lookup1(kPsaShouldBeacon)); |
| if (options->MatchesDownstreamCacheRebeaconingKey(should_beacon)) { |
| ctx->preserve_caching_headers = kDontPreserveHeaders; |
| } |
| } |
| |
| ctx->recorder = NULL; |
| ctx->url_string = url_string; |
| |
| // Set up a cleanup handler on the request. |
| ngx_http_cleanup_t* cleanup = ngx_http_cleanup_add(r, 0); |
| if (cleanup == NULL) { |
| ps_release_request_context(ctx); |
| return NGX_ERROR; |
| } |
| cleanup->handler = ps_release_request_context; |
| cleanup->data = ctx; |
| ngx_http_set_ctx(r, ctx, ngx_pagespeed); |
| } |
| |
| if (pagespeed_resource) { |
| // TODO(jefftk): Set using_spdy appropriately. See |
| // ProxyInterface::ProxyRequestCallback |
| ps_create_base_fetch(ctx, request_context, request_headers.release()); |
| ResourceFetch::Start( |
| url, |
| custom_options.release() /* null if there aren't custom options */, |
| false /* using_spdy */, cfg_s->server_context, ctx->base_fetch); |
| return ps_async_wait_response(r); |
| } else if (is_an_admin_handler) { |
| ps_create_base_fetch(ctx, request_context, request_headers.release()); |
| QueryParams query_params; |
| query_params.ParseFromUrl(url); |
| |
| PosixTimer timer; |
| int64 now_ms = timer.NowMs(); |
| ctx->base_fetch->response_headers()->SetDateAndCaching( |
| now_ms, 0 /* max-age */, ", no-cache"); |
| |
| if (response_category == RequestRouting::kStatistics || |
| response_category == RequestRouting::kGlobalStatistics) { |
| cfg_s->server_context->StatisticsPage( |
| response_category == RequestRouting::kGlobalStatistics, |
| query_params, |
| cfg_s->server_context->config(), |
| ctx->base_fetch); |
| } else if (response_category == RequestRouting::kConsole) { |
| cfg_s->server_context->ConsoleHandler( |
| *cfg_s->server_context->config(), |
| AdminSite::kStatistics, |
| query_params, |
| ctx->base_fetch); |
| } else if (response_category == RequestRouting::kAdmin || |
| response_category == RequestRouting::kGlobalAdmin) { |
| cfg_s->server_context->AdminPage( |
| response_category == RequestRouting::kGlobalAdmin, |
| url, |
| query_params, |
| custom_options == NULL ? cfg_s->server_context->config() |
| : custom_options.get(), |
| ctx->base_fetch); |
| } else if (response_category == RequestRouting::kCachePurge) { |
| AdminSite* admin_site = cfg_s->server_context->admin_site(); |
| admin_site->PurgeHandler(url_string, |
| cfg_s->server_context->cache_path(), |
| ctx->base_fetch); |
| } else { |
| CHECK(false); |
| } |
| |
| return ps_async_wait_response(r); |
| } |
| |
| if (html_rewrite) { |
| ps_create_base_fetch(ctx, request_context, request_headers.release()); |
| // Do not store driver in request_context, it's not safe. |
| RewriteDriver* driver; |
| |
| // If we don't have custom options we can use NewRewriteDriver which reuses |
| // rewrite drivers and so is faster because there's no wait to construct |
| // them. Otherwise we have to build a new one every time. |
| |
| if (custom_options.get() == NULL) { |
| driver = cfg_s->server_context->NewRewriteDriver( |
| ctx->base_fetch->request_context()); |
| } else { |
| // NewCustomRewriteDriver takes ownership of custom_options. |
| driver = cfg_s->server_context->NewCustomRewriteDriver( |
| custom_options.release(), ctx->base_fetch->request_context()); |
| } |
| |
| StringPiece user_agent = ctx->base_fetch->request_headers()->Lookup1( |
| HttpAttributes::kUserAgent); |
| if (!user_agent.empty()) { |
| driver->SetUserAgent(user_agent); |
| } |
| driver->SetRequestHeaders(*ctx->base_fetch->request_headers()); |
| driver->set_pagespeed_query_params(pagespeed_query_params); |
| driver->set_pagespeed_option_cookies(pagespeed_option_cookies); |
| |
| // TODO(jefftk): FlushEarlyFlow would go here. |
| bool page_callback_added = false; |
| ProxyFetchPropertyCallbackCollector* property_callback = |
| ProxyFetchFactory::InitiatePropertyCacheLookup( |
| !html_rewrite /* is_resource_fetch */, |
| url, |
| cfg_s->server_context, |
| options, |
| ctx->base_fetch, |
| false /* requires_blink_cohort (no longer unused) */, |
| &page_callback_added); |
| |
| // Will call StartParse etc. The rewrite driver will take care of deleting |
| // itself if necessary. |
| ctx->proxy_fetch = cfg_s->proxy_fetch_factory->CreateNewProxyFetch( |
| url_string, ctx->base_fetch, driver, |
| property_callback, |
| NULL /* original_content_fetch */); |
| return NGX_OK; |
| } |
| |
| if (options->in_place_rewriting_enabled() && |
| options->enabled() && |
| options->IsAllowed(url.Spec())) { |
| ps_create_base_fetch(ctx, request_context, request_headers.release()); |
| // Do not store driver in request_context, it's not safe. |
| RewriteDriver* driver; |
| if (custom_options.get() == NULL) { |
| driver = cfg_s->server_context->NewRewriteDriver( |
| ctx->base_fetch->request_context()); |
| } else { |
| // NewCustomRewriteDriver takes ownership of custom_options. |
| driver = cfg_s->server_context->NewCustomRewriteDriver( |
| custom_options.release(), ctx->base_fetch->request_context()); |
| } |
| |
| StringPiece user_agent = ctx->base_fetch->request_headers()->Lookup1( |
| HttpAttributes::kUserAgent); |
| if (!user_agent.empty()) { |
| driver->SetUserAgent(user_agent); |
| } |
| driver->SetRequestHeaders(*ctx->base_fetch->request_headers()); |
| |
| ctx->driver = driver; |
| |
| cfg_s->server_context->message_handler()->Message( |
| kInfo, "Trying to serve rewritten resource in-place: %s", |
| url_string.c_str()); |
| |
| ctx->in_place = true; |
| ctx->base_fetch->set_ipro_lookup(true); |
| ctx->driver->FetchInPlaceResource( |
| url, false /* proxy_mode */, ctx->base_fetch); |
| |
| return ps_async_wait_response(r); |
| } |
| |
| // NOTE: We are using the below debug message as is for some of our system |
| // tests. So, be careful about test breakages caused by changing or |
| // removing this line. |
| ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, |
| "Passing on content handling for non-pagespeed resource '%s'", |
| url_string.c_str()); |
| |
| CHECK(ctx->base_fetch == NULL); |
| // set html_rewrite flag. |
| ctx->html_rewrite = true; |
| return NGX_DECLINED; |
| } |
| |
| // Send each buffer in the chain to the proxy_fetch for optimization. |
| // Eventually it will make it's way, optimized, to base_fetch. |
| void ps_send_to_pagespeed(ngx_http_request_t* r, |
| ps_request_ctx_t* ctx, |
| ps_srv_conf_t* cfg_s, |
| ngx_chain_t* in) { |
| ngx_chain_t* cur; |
| int last_buf = 0; |
| for (cur = in; cur != NULL; cur = cur->next) { |
| last_buf = cur->buf->last_buf; |
| |
| // Buffers are not really the last buffer until they've been through |
| // pagespeed. |
| cur->buf->last_buf = 0; |
| |
| CHECK(ctx->proxy_fetch != NULL); |
| if (ctx->inflater_ == NULL) { |
| ctx->proxy_fetch->Write( |
| StringPiece(reinterpret_cast<char*>(cur->buf->pos), |
| cur->buf->last - cur->buf->pos), cfg_s->handler); |
| } else { |
| char buf[kStackBufferSize]; |
| ctx->inflater_->SetInput(reinterpret_cast<char*>(cur->buf->pos), |
| cur->buf->last - cur->buf->pos); |
| while (ctx->inflater_->HasUnconsumedInput()) { |
| int num_inflated_bytes = ctx->inflater_->InflateBytes( |
| buf, kStackBufferSize); |
| if (num_inflated_bytes < 0) { |
| cfg_s->handler->Message(kWarning, "Corrupted inflation"); |
| } else if (num_inflated_bytes > 0) { |
| ctx->proxy_fetch->Write(StringPiece(buf, num_inflated_bytes), |
| cfg_s->handler); |
| } |
| } |
| } |
| // We're done with buffers as we pass them through, so mark them as sent as |
| // we go. |
| cur->buf->pos = cur->buf->last; |
| } |
| |
| if (last_buf) { |
| ctx->proxy_fetch->Done(true /* success */); |
| ctx->proxy_fetch = NULL; // ProxyFetch deletes itself on Done(). |
| } else { |
| // TODO(jefftk): Decide whether Flush() is warranted here. |
| ctx->proxy_fetch->Flush(cfg_s->handler); |
| } |
| } |
| |
| #ifndef ngx_http_clear_etag |
| // The ngx_http_clear_etag(r) macro was added in 1.3.3. Backport it if it's not |
| // present. |
| #define ngx_http_clear_etag(r) \ |
| if (r->headers_out.etag) { \ |
| r->headers_out.etag->hash = 0; \ |
| r->headers_out.etag = NULL; \ |
| } |
| #endif |
| |
| void ps_strip_html_headers(ngx_http_request_t* r) { |
| // We're modifying content, so switch to 'Transfer-Encoding: chunked' and |
| // calculate on the fly. |
| ngx_http_clear_content_length(r); |
| |
| ngx_table_elt_t* header; |
| NgxListIterator it(&(r->headers_out.headers.part)); |
| while ((header = it.Next()) != NULL) { |
| // We also need to strip: |
| // Accept-Ranges |
| // - won't work because our html changes |
| // Vary: Accept-Encoding |
| // - our gzip filter will add this later |
| if (STR_CASE_EQ_LITERAL(header->key, "Accept-Ranges") || |
| (STR_CASE_EQ_LITERAL(header->key, "Vary") && |
| STR_CASE_EQ_LITERAL(header->value, "Accept-Encoding"))) { |
| // Response headers with hash of 0 are excluded from the response. |
| header->hash = 0; |
| } |
| } |
| } |
| |
| // Returns true, if the the response headers indicate there are multiple |
| // content encodings. |
| bool ps_has_stacked_content_encoding(ngx_http_request_t* r) { |
| ngx_uint_t i; |
| ngx_list_part_t* part = &(r->headers_out.headers.part); |
| ngx_table_elt_t* header = static_cast<ngx_table_elt_t*>(part->elts); |
| int field_count = 0; |
| |
| for (i = 0 ; /* void */; i++) { |
| if (i >= part->nelts) { |
| if (part->next == NULL) { |
| break; |
| } |
| |
| part = part->next; |
| header = static_cast<ngx_table_elt_t*>(part->elts); |
| i = 0; |
| } |
| |
| // Inspect Content-Encoding headers, checking all value fields |
| // If an origin returns gzip,foo, that is what we will get here. |
| if (STR_CASE_EQ_LITERAL(header[i].key, "Content-Encoding")) { |
| if (header[i].value.data != NULL && header[i].value.len > 0) { |
| char* p = reinterpret_cast<char*>(header[i].value.data); |
| ngx_uint_t j; |
| for (j = 0; j < header[i].value.len; j++) { |
| if (p[j] == ',' || j == header[i].value.len - 1) { |
| field_count++; |
| } |
| } |
| if (field_count > 1) { |
| return true; |
| } |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| ngx_int_t ps_etag_header_filter(ngx_http_request_t* r) { |
| u_char* etag = reinterpret_cast<u_char*>( |
| const_cast<char*>(kInternalEtagName)); |
| ngx_table_elt_t* header; |
| NgxListIterator it(&(r->headers_out.headers.part)); |
| while ((header = it.Next()) != NULL) { |
| if (header->key.len == strlen(kInternalEtagName) && |
| !ngx_strncasecmp(header->key.data, etag, header->key.len)) { |
| header->key.data = reinterpret_cast<u_char*>(const_cast<char*>("ETag")); |
| header->key.len = 4; |
| r->headers_out.etag = header; |
| break; |
| } |
| } |
| return ngx_http_ef_next_header_filter(r); |
| } |
| |
| namespace html_rewrite { |
| ngx_http_output_header_filter_pt ngx_http_next_header_filter; |
| ngx_http_output_body_filter_pt ngx_http_next_body_filter; |
| |
| // After pagespeed has had a chance to run, copy the headers it produced to |
| // nginx so it can send them out to the browser. |
| ngx_int_t ps_html_rewrite_header_filter(ngx_http_request_t* r) { |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| if (cfg_s->server_context == NULL) { |
| // Pagespeed is on for some server block but not this one. |
| return ngx_http_next_header_filter(r); |
| } |
| |
| if (r != r->main) { |
| // Don't handle subrequests. |
| return ngx_http_next_header_filter(r); |
| } |
| // Poll for cache flush on every request (polls are rate-limited). |
| cfg_s->server_context->FlushCacheIfNecessary(); |
| |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| |
| if (ctx == NULL || ctx->html_rewrite == false) { |
| return ngx_http_next_header_filter(r); |
| } |
| |
| if (r->err_status != 0) { |
| ctx->html_rewrite = false; |
| return ngx_http_next_header_filter(r); |
| } |
| |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "http pagespeed html rewrite header filter \"%V\"", &r->uri); |
| |
| // We don't know what this request is, but we only want to send html through |
| // to pagespeed. Check the content type header and find out. |
| const ContentType* content_type = |
| MimeTypeToContentType( |
| str_to_string_piece(r->headers_out.content_type)); |
| if (content_type == NULL || !content_type->IsHtmlLike()) { |
| // Unknown or otherwise non-html content type: skip it. |
| ctx->html_rewrite = false; |
| return ngx_http_next_header_filter(r); |
| } |
| |
| ngx_int_t rc = ps_resource_handler(r, true /* html rewrite */, |
| RequestRouting::kResource); |
| if (rc != NGX_OK) { |
| ctx->html_rewrite = false; |
| return ngx_http_next_header_filter(r); |
| } |
| |
| if (r->headers_out.content_encoding && |
| r->headers_out.content_encoding->value.len) { |
| // headers_out.content_encoding will be set to the exact last |
| // Content-Encoding response header value that nginx receives. To |
| // check if there were multiple (aka stacked) encodings in the |
| // response headers, we must iterate them all. |
| if (!ps_has_stacked_content_encoding(r)) { |
| StringPiece content_encoding = |
| str_to_string_piece(r->headers_out.content_encoding->value); |
| GzipInflater::InflateType inflate_type = GzipInflater::kGzip; |
| bool is_encoded = false; |
| if (StringCaseEqual(content_encoding, "deflate")) { |
| is_encoded = true; |
| inflate_type = GzipInflater::kDeflate; |
| } else if (StringCaseEqual(content_encoding, "gzip")) { |
| is_encoded = true; |
| } |
| |
| if (is_encoded) { |
| r->headers_out.content_encoding->hash = 0; |
| r->headers_out.content_encoding = NULL; |
| ctx->inflater_ = new GzipInflater(inflate_type); |
| ctx->inflater_->Init(); |
| } |
| } |
| } |
| |
| ps_strip_html_headers(r); |
| |
| // TODO(jefftk): is this thread safe? |
| copy_response_headers_from_ngx(r, ctx->base_fetch->response_headers()); |
| |
| ps_set_buffered(r, true); |
| r->filter_need_in_memory = 1; |
| return NGX_AGAIN; |
| } |
| |
| ngx_int_t ps_html_rewrite_body_filter(ngx_http_request_t* r, ngx_chain_t* in) { |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| if (cfg_s->server_context == NULL) { |
| // Pagespeed is on for some server block but not this one. |
| return ngx_http_next_body_filter(r, in); |
| } |
| |
| if (r != r->main) { |
| // Don't handle subrequests. |
| return ngx_http_next_body_filter(r, in); |
| } |
| // Don't need to check for a cache flush; already did in |
| // ps_html_rewrite_header_filter. |
| |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| |
| if (ctx == NULL || ctx->html_rewrite == false) { |
| // ctx is null iff we've decided to pass through this request unchanged. |
| return ngx_http_next_body_filter(r, in); |
| } |
| |
| // We don't want to handle requests with errors, but we should be dealing with |
| // that in the header filter and not initializing ctx. |
| CHECK(r->err_status == 0); // NOLINT |
| |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "http pagespeed html rewrite body filter \"%V\"", &r->uri); |
| |
| |
| if (in != NULL) { |
| // Send all input data to the proxy fetch. |
| ps_send_to_pagespeed(r, ctx, cfg_s, in); |
| } |
| |
| return ngx_http_next_body_filter(r, NULL); |
| } |
| |
| void ps_html_rewrite_filter_init() { |
| ngx_http_next_header_filter = ngx_http_top_header_filter; |
| ngx_http_top_header_filter = ps_html_rewrite_header_filter; |
| |
| ngx_http_next_body_filter = ngx_http_top_body_filter; |
| ngx_http_top_body_filter = ps_html_rewrite_body_filter; |
| } |
| |
| } // namespace html_rewrite |
| |
| using html_rewrite::ps_html_rewrite_filter_init; |
| |
| namespace in_place { |
| |
| ngx_http_output_header_filter_pt ngx_http_next_header_filter; |
| ngx_http_output_body_filter_pt ngx_http_next_body_filter; |
| |
| // Header filter to support IPRO. |
| // |
| // The control flow here is tricky, probably excessively so. |
| ngx_int_t ps_in_place_check_header_filter(ngx_http_request_t* r) { |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| |
| if (ctx == NULL) { |
| return ngx_http_next_header_filter(r); |
| } |
| |
| if (ctx->recorder != NULL) { |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "ps in place check header filter recording: %V", &r->uri); |
| |
| CHECK(!ctx->in_place); |
| |
| // We didn't find this resource in cache originally, so we're recording it |
| // as it passes us by. At this point the headers from things that run |
| // before us are set but not things that run after us, which means here is |
| // where we need to check whether there's a "Content-Encoding: gzip". If we |
| // waited to do this in ps_in_place_body_filter we wouldn't be able to tell |
| // the difference between response headers that have "C-E: gz" because we're |
| // proxying for an upstream that gzipped the content and response headers |
| // that have it because the gzip filter (which runs after us) is going to |
| // produce gzipped output. |
| // |
| // The recorder will do this checking, so pass it the headers. |
| ResponseHeaders response_headers; |
| copy_response_headers_from_ngx(r, &response_headers); |
| ctx->recorder->ConsiderResponseHeaders( |
| InPlaceResourceRecorder::kPreliminaryHeaders, &response_headers); |
| return ngx_http_next_header_filter(r); |
| } |
| |
| if (!ctx->in_place) { |
| return ngx_http_next_header_filter(r); |
| } |
| |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "ps in place check header filter initial: %V", &r->uri); |
| |
| int status_code = r->headers_out.status; |
| bool status_ok = (status_code != 0) && (status_code < 400); |
| |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| NgxServerContext* server_context = cfg_s->server_context; |
| MessageHandler* message_handler = cfg_s->handler; |
| GoogleString url = ps_determine_url(r); |
| // The URL we use for cache key is a bit different since it may |
| // have PageSpeed query params removed. |
| GoogleString cache_url = ctx->url_string; |
| |
| // continue process |
| if (status_ok) { |
| ctx->in_place = false; |
| |
| server_context->rewrite_stats()->ipro_served()->Add(1); |
| message_handler->Message( |
| kInfo, "Serving rewritten resource in-place: %s", |
| url.c_str()); |
| |
| return ngx_http_next_header_filter(r); |
| } |
| |
| if (status_code == CacheUrlAsyncFetcher::kNotInCacheStatus && |
| !r->header_only) { |
| server_context->rewrite_stats()->ipro_not_in_cache()->Add(1); |
| server_context->message_handler()->Message( |
| kInfo, |
| "Could not rewrite resource in-place " |
| "because URL is not in cache: %s", |
| cache_url.c_str()); |
| const SystemRewriteOptions* options = SystemRewriteOptions::DynamicCast( |
| ctx->driver->options()); |
| RequestContextPtr request_context( |
| cfg_s->server_context->NewRequestContext(r)); |
| request_context->set_options(options->ComputeHttpOptions()); |
| RequestHeaders request_headers; |
| copy_request_headers_from_ngx(r, &request_headers); |
| // This URL was not found in cache (neither the input resource nor |
| // a ResourceNotCacheable entry) so we need to get it into cache |
| // (or at least a note that it cannot be cached stored there). |
| // We do that using an Apache output filter. |
| ctx->recorder = new InPlaceResourceRecorder( |
| request_context, |
| cache_url, |
| ctx->driver->CacheFragment(), |
| request_headers.GetProperties(), |
| options->ipro_max_response_bytes(), |
| options->ipro_max_concurrent_recordings(), |
| server_context->http_cache(), |
| server_context->statistics(), |
| message_handler); |
| // set in memory flag for in place_body_filter |
| r->filter_need_in_memory = 1; |
| |
| // We don't have the response headers at all yet because we haven't yet gone |
| // to the backend. |
| } else { |
| server_context->rewrite_stats()->ipro_not_rewritable()->Add(1); |
| message_handler->Message( |
| kInfo, "Could not rewrite resource in-place: %s", url.c_str()); |
| } |
| |
| ctx->driver->Cleanup(); |
| ctx->driver = NULL; |
| // enable html_rewrite |
| ctx->html_rewrite = true; |
| ctx->in_place = false; |
| |
| return ps_decline_request(r); |
| } |
| |
| // If we've decided that we should record this response for future optimization |
| // with IPRO, then log the bytes as they come through |
| ngx_int_t ps_in_place_body_filter(ngx_http_request_t* r, ngx_chain_t* in) { |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| if (ctx == NULL || ctx->recorder == NULL) { |
| return ngx_http_next_body_filter(r, in); |
| } |
| |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "ps in place body filter: %V", &r->uri); |
| |
| InPlaceResourceRecorder* recorder = ctx->recorder; |
| for (ngx_chain_t* cl = in; cl; cl = cl->next) { |
| if (ngx_buf_size(cl->buf)) { |
| CHECK(ngx_buf_in_memory(cl->buf)); |
| StringPiece contents(reinterpret_cast<char*>(cl->buf->pos), |
| ngx_buf_size(cl->buf)); |
| recorder->Write(contents, recorder->handler()); |
| } |
| |
| if (cl->buf->flush) { |
| recorder->Flush(recorder->handler()); |
| } |
| |
| if (cl->buf->last_buf || recorder->failed()) { |
| ResponseHeaders response_headers; |
| copy_response_headers_from_ngx(r, &response_headers); |
| ctx->recorder->DoneAndSetHeaders(&response_headers); |
| ctx->recorder = NULL; |
| break; |
| } |
| } |
| |
| return ngx_http_next_body_filter(r, in); |
| } |
| |
| void ps_in_place_filter_init() { |
| ngx_http_next_header_filter = ngx_http_top_header_filter; |
| ngx_http_top_header_filter = ps_in_place_check_header_filter; |
| |
| ngx_http_next_body_filter = ngx_http_top_body_filter; |
| ngx_http_top_body_filter = ps_in_place_body_filter; |
| } |
| |
| } // namespace in_place |
| |
| using in_place::ps_in_place_filter_init; |
| |
| ngx_int_t send_out_headers_and_body( |
| ngx_http_request_t* r, |
| const ResponseHeaders& response_headers, |
| const GoogleString& output) { |
| ngx_int_t rc = copy_response_headers_to_ngx( |
| r, response_headers, kDontPreserveHeaders); |
| |
| if (rc != NGX_OK) { |
| return NGX_ERROR; |
| } |
| |
| rc = ngx_http_send_header(r); |
| |
| if (rc != NGX_OK) { |
| return NGX_ERROR; |
| } |
| |
| // Send the body. |
| ngx_chain_t* out; |
| rc = string_piece_to_buffer_chain( |
| r->pool, output, &out, true /* send_last_buf */); |
| if (rc == NGX_ERROR) { |
| return NGX_ERROR; |
| } |
| CHECK(rc == NGX_OK); |
| |
| return ngx_http_output_filter(r, out); |
| } |
| |
| // Handle responses where we have the content we need in memory and can just |
| // send it right out. |
| ngx_int_t ps_simple_handler(ngx_http_request_t* r, |
| NgxServerContext* server_context, |
| RequestRouting::Response response_category) { |
| NgxRewriteDriverFactory* factory = |
| static_cast<NgxRewriteDriverFactory*>( |
| server_context->factory()); |
| NgxMessageHandler* message_handler = factory->ngx_message_handler(); |
| StringPiece request_uri_path = str_to_string_piece(r->uri); |
| |
| GoogleString url_string = ps_determine_url(r); |
| GoogleUrl url(url_string); |
| QueryParams query_params; |
| if (url.IsWebValid()) { |
| query_params.ParseFromUrl(url); |
| } |
| |
| GoogleString output; |
| StringWriter writer(&output); |
| HttpStatus::Code status = HttpStatus::kOK; |
| ContentType content_type = kContentTypeHtml; |
| StringPiece cache_control = HttpAttributes::kNoCache; |
| const char* error_message = NULL; |
| |
| switch (response_category) { |
| case RequestRouting::kStaticContent: { |
| StringPiece file_contents; |
| if (!server_context->static_asset_manager()->GetAsset( |
| request_uri_path.substr(factory->static_asset_prefix().length()), |
| &file_contents, &content_type, &cache_control)) { |
| return NGX_DECLINED; |
| } |
| file_contents.CopyToString(&output); |
| break; |
| } |
| case RequestRouting::kMessages: { |
| GoogleString log; |
| StringWriter log_writer(&log); |
| if (!message_handler->Dump(&log_writer)) { |
| writer.Write("Writing to ngx_pagespeed_message failed. \n" |
| "Please check if it's enabled in pagespeed.conf.\n", |
| message_handler); |
| } else { |
| HtmlKeywords::WritePre(log, "", &writer, message_handler); |
| } |
| break; |
| } |
| default: |
| ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, |
| "ps_simple_handler: unknown RequestRouting."); |
| return NGX_ERROR; |
| } |
| |
| if (error_message != NULL) { |
| status = HttpStatus::kNotFound; |
| content_type = kContentTypeHtml; |
| output = error_message; |
| } |
| |
| ResponseHeaders response_headers; |
| response_headers.SetStatusAndReason(status); |
| response_headers.set_major_version(1); |
| response_headers.set_minor_version(1); |
| |
| response_headers.Add(HttpAttributes::kContentType, content_type.mime_type()); |
| // http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx |
| // Script and styleSheet elements will reject responses with |
| // incorrect MIME types if the server sends the response header |
| // "X-Content-Type-Options: nosniff". This is a security feature |
| // that helps prevent attacks based on MIME-type confusion. |
| response_headers.Add("X-Content-Type-Options", "nosniff"); |
| |
| int64 now_ms = factory->timer()->NowMs(); |
| response_headers.SetDate(now_ms); |
| response_headers.SetLastModified(now_ms); |
| response_headers.Add(HttpAttributes::kCacheControl, cache_control); |
| |
| char* cache_control_s = string_piece_to_pool_string(r->pool, cache_control); |
| if (cache_control_s != NULL) { |
| if (FindIgnoreCase(cache_control, "private") == StringPiece::npos) { |
| response_headers.Add(HttpAttributes::kEtag, "W/\"0\""); |
| } |
| } |
| |
| send_out_headers_and_body(r, response_headers, output); |
| return NGX_OK; |
| } |
| |
| void ps_beacon_handler_helper(ngx_http_request_t* r, |
| StringPiece beacon_data) { |
| ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "ps_beacon_handler_helper: beacon[%d] %*s", |
| beacon_data.size(), beacon_data.size(), |
| beacon_data.data()); |
| |
| StringPiece user_agent; |
| if (r->headers_in.user_agent != NULL) { |
| user_agent = str_to_string_piece(r->headers_in.user_agent->value); |
| } |
| |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| CHECK(cfg_s != NULL); |
| |
| RequestContextPtr request_context( |
| cfg_s->server_context->NewRequestContext(r)); |
| // TODO(sligocki): Do we want custom options here? It probably doesn't matter |
| // for beacons. |
| request_context->set_options( |
| cfg_s->server_context->global_options()->ComputeHttpOptions()); |
| |
| cfg_s->server_context->HandleBeacon(beacon_data, |
| user_agent, |
| request_context); |
| |
| ps_set_cache_control(r, const_cast<char*>("max-age=0, no-cache")); |
| |
| // TODO(jefftk): figure out how to insert Content-Length:0 as a response |
| // header so wget doesn't hang. |
| } |
| |
| // Load the request body into out. ngx_http_read_client_request_body must |
| // already have been called. Return false on failure, true on success. |
| bool ps_request_body_to_string_piece( |
| ngx_http_request_t* r, StringPiece* out) { |
| if (r->request_body == NULL || r->request_body->bufs == NULL) { |
| ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, |
| "ps_request_body_to_string_piece: " |
| "empty request body."); |
| return false; |
| } |
| |
| if (r->request_body->temp_file) { |
| u_char buf[POST_BUF_READ_SIZE]; |
| int count = 0; |
| ssize_t ret; |
| GoogleString tmp; |
| |
| // Note that we depend on nginx to impose sensible limits on post data. |
| while ((ret = ngx_read_file(&r->request_body->temp_file->file, |
| buf, POST_BUF_READ_SIZE, count)) > 0) { |
| tmp.append(reinterpret_cast<char*>(buf), ret); |
| count += ret; |
| } |
| |
| if (ret < 0) { |
| ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, |
| "ps_request_body_to_string_piece: " |
| "error reading post body."); |
| return false; |
| } |
| |
| // We have read the complete post body, copy it to a buffer |
| // from the request's pool. |
| u_char* data = reinterpret_cast<u_char*>(ngx_palloc(r->pool, count)); |
| memcpy(data, tmp.c_str(), count); |
| *out = StringPiece(reinterpret_cast<char*>(data), count); |
| |
| return true; |
| } else if (r->request_body->bufs->next == NULL) { |
| // There's just one buffer, so we can simply return a StringPiece pointing |
| // to this buffer. |
| ngx_buf_t* buffer = r->request_body->bufs->buf; |
| CHECK(!buffer->in_file); |
| int len = buffer->last - buffer->pos; |
| ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "ngx_pagespeed beacon: single buffer of %d", len); |
| *out = StringPiece(reinterpret_cast<char*>(buffer->pos), len); |
| return true; |
| } else { |
| // There are multiple buffers, so we need to allocate memory for a string to |
| // hold the whole result. This should only happen when the POST is sent |
| // with "Transfer-Encoding: Chunked". |
| |
| // First determine how much data there is. |
| int len = 0; |
| int buffers = 0; |
| |
| ngx_chain_t* chain_link; |
| for (chain_link = r->request_body->bufs; |
| chain_link != NULL; |
| chain_link = chain_link->next) { |
| len += chain_link->buf->last - chain_link->buf->pos; |
| buffers++; |
| } |
| |
| ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "ngx_pagespeed beacon: %d buffers totalling %d", len); |
| |
| // Allocate a string to store the combined result. |
| u_char* s = static_cast<u_char*>(ngx_palloc(r->pool, len)); |
| if (s == NULL) { |
| ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, |
| "ps_request_body_to_string_piece: " |
| "failed to allocate memory"); |
| return false; |
| } |
| |
| // Copy the data into the combined string. |
| u_char* current_position = s; |
| int i; |
| for (chain_link = r->request_body->bufs, i = 0; |
| chain_link != NULL; |
| chain_link = chain_link->next, i++) { |
| ngx_buf_t* buffer = chain_link->buf; |
| CHECK(!buffer->in_file); |
| current_position = ngx_copy(current_position, buffer->pos, |
| buffer->last - buffer->pos); |
| } |
| CHECK_EQ(current_position, s + len); |
| *out = StringPiece(reinterpret_cast<char*>(s), len); |
| return true; |
| } |
| } |
| |
| // Parses out query params from the request. |
| void ps_query_params_handler(ngx_http_request_t* r, StringPiece* data) { |
| StringPiece unparsed_uri = str_to_string_piece(r->unparsed_uri); |
| stringpiece_ssize_type question_mark_index = unparsed_uri.find("?"); |
| if (question_mark_index == StringPiece::npos) { |
| *data = ""; |
| } else { |
| *data = unparsed_uri.substr( |
| question_mark_index+1, unparsed_uri.size() - (question_mark_index+1)); |
| } |
| } |
| |
| // Called after nginx reads the request body from the client. For another |
| // example processing request buffers, see ngx_http_form_input_module.c |
| void ps_beacon_body_handler(ngx_http_request_t* r) { |
| // Even if the beacon is a POST, the originating url should be in the query |
| // params, not the POST body. |
| StringPiece query_param_beacon_data; |
| ps_query_params_handler(r, &query_param_beacon_data); |
| |
| StringPiece request_body; |
| bool ok = ps_request_body_to_string_piece(r, &request_body); |
| GoogleString beacon_data = StrCat( |
| query_param_beacon_data, "&", request_body); |
| if (ok) { |
| ps_beacon_handler_helper(r, beacon_data.c_str()); |
| ngx_http_finalize_request(r, NGX_HTTP_NO_CONTENT); |
| } else { |
| ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| } |
| } |
| |
| // We need to get the beacon data to ps_beacon_body_handler so it can pass it |
| // along to SystemServerContext::HandleBeacon, but it might have been POSTed. |
| // If it's posted we need to make an async call to get the data, otherwise just |
| // read it out of the query params. |
| ngx_int_t ps_beacon_handler(ngx_http_request_t* r) { |
| if (r->method == NGX_HTTP_POST) { |
| // Use post body. Handler functions are called before the request body has |
| // been read from the client, so we need to ask nginx to read it from the |
| // client and then call us back. Control flow continues in |
| // ps_beacon_body_handler unless there's an error reading the request body. |
| // |
| // See: http://forum.nginx.org/read.php?2,31312,31312 |
| ngx_int_t rc = ngx_http_read_client_request_body(r, ps_beacon_body_handler); |
| if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { |
| return rc; |
| } |
| return NGX_DONE; |
| } else { |
| // Use query params. |
| StringPiece query_param_beacon_data; |
| ps_query_params_handler(r, &query_param_beacon_data); |
| ps_beacon_handler_helper(r, query_param_beacon_data); |
| return NGX_HTTP_NO_CONTENT; |
| } |
| } |
| |
| // Some things pagespeed filters on the way past (html) and other things it |
| // actually handles, like requests for resources |
| // (example.css.pagespeed.ce.LyfcM6Wulf.css) and static content |
| // (/ngx_pagespeed_static/js_defer.q1EBmcgYOC.js). This is called once we know |
| // we need to handle a resource, and it figures out which particular handler can |
| // supply it to the user. |
| ngx_int_t ps_content_handler(ngx_http_request_t* r) { |
| ps_srv_conf_t* cfg_s = ps_get_srv_config(r); |
| if (cfg_s->server_context == NULL) { |
| // Pagespeed is on for some server block but not this one. |
| return NGX_DECLINED; |
| } |
| |
| // Poll for cache flush on every request (polls are rate-limited). |
| cfg_s->server_context->FlushCacheIfNecessary(); |
| |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "http pagespeed handler \"%V\"", &r->uri); |
| |
| RequestRouting::Response response_category = |
| ps_route_request(r, true /* is a resource fetch */); |
| switch (response_category) { |
| case RequestRouting::kError: |
| return NGX_ERROR; |
| case RequestRouting::kNotUnderstood: |
| case RequestRouting::kPagespeedDisabled: |
| case RequestRouting::kInvalidUrl: |
| case RequestRouting::kPagespeedSubrequest: |
| case RequestRouting::kNotHeadOrGet: |
| case RequestRouting::kErrorResponse: |
| return NGX_DECLINED; |
| case RequestRouting::kBeacon: |
| return ps_beacon_handler(r); |
| case RequestRouting::kStaticContent: |
| case RequestRouting::kMessages: |
| return ps_simple_handler(r, cfg_s->server_context, response_category); |
| case RequestRouting::kStatistics: |
| case RequestRouting::kGlobalStatistics: |
| case RequestRouting::kConsole: |
| case RequestRouting::kAdmin: |
| case RequestRouting::kGlobalAdmin: |
| case RequestRouting::kCachePurge: |
| case RequestRouting::kResource: |
| return ps_resource_handler( |
| r, false /* html rewrite */, response_category); |
| } |
| |
| CHECK(0); |
| return NGX_ERROR; |
| } |
| |
| ngx_int_t ps_phase_handler(ngx_http_request_t* r, |
| ngx_http_phase_handler_t* ph) { |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| "pagespeed phase: %ui", r->phase_handler); |
| |
| r->write_event_handler = ngx_http_request_empty_handler; |
| |
| ngx_int_t rc = ps_content_handler(r); |
| // Warning: this requires ps_content_handler to always return NGX_DECLINED |
| // directly if it's not going to handle the request. It is not ok for |
| // ps_content_handler to asynchronously determine whether to handle the |
| // request, returning NGX_DONE here. |
| if (rc == NGX_DECLINED) { |
| r->write_event_handler = ngx_http_core_run_phases; |
| r->phase_handler++; |
| return NGX_AGAIN; |
| } |
| |
| ngx_http_finalize_request(r, rc); |
| return NGX_OK; |
| } |
| |
| namespace fix_headers { |
| ngx_http_output_header_filter_pt ngx_http_next_header_filter; |
| |
| // Clear a few headers from html responses. |
| |
| ngx_int_t ps_html_rewrite_fix_headers_filter(ngx_http_request_t* r) { |
| ps_request_ctx_t* ctx = ps_get_request_context(r); |
| if (r != r->main || ctx == NULL || !ctx->html_rewrite || |
| ctx->preserve_caching_headers == kPreserveAllCachingHeaders) { |
| return ngx_http_next_header_filter(r); |
| } |
| if (ctx->preserve_caching_headers == kDontPreserveHeaders) { |
| // Don't cache html. See mod_instaweb:instaweb_fix_headers_filter. |
| NgxCachingHeaders caching_headers(r); |
| ps_set_cache_control(r, string_piece_to_pool_string( |
| r->pool, caching_headers.GenerateDisabledCacheControl())); |
| } |
| |
| // Pagespeed html doesn't need etags: it should never be cached. |
| ngx_http_clear_etag(r); |
| |
| // An html page may change without the underlying file changing, because of |
| // how resources are included. Pagespeed adds cache control headers for |
| // resources instead of using the last modified header. |
| ngx_http_clear_last_modified(r); |
| |
| // Clear expires |
| if (r->headers_out.expires) { |
| r->headers_out.expires->hash = 0; |
| r->headers_out.expires = NULL; |
| } |
| |
| return ngx_http_next_header_filter(r); |
| } |
| |
| void ps_html_rewrite_fix_headers_filter_init() { |
| ngx_http_next_header_filter = ngx_http_top_header_filter; |
| ngx_http_top_header_filter = ps_html_rewrite_fix_headers_filter; |
| } |
| |
| } // namespace fix_headers |
| |
| using fix_headers::ps_html_rewrite_fix_headers_filter_init; |
| |
| |
| // preaccess_handler should be at generic phase before try_files |
| ngx_int_t ps_preaccess_handler(ngx_http_request_t* r) { |
| ngx_http_core_main_conf_t* cmcf; |
| ngx_http_phase_handler_t* ph; |
| ngx_uint_t i; |
| |
| cmcf = static_cast<ngx_http_core_main_conf_t*>( |
| ngx_http_get_module_main_conf(r, ngx_http_core_module)); |
| |
| ph = cmcf->phase_engine.handlers; |
| |
| i = r->phase_handler; |
| // move handlers before try_files && content phase |
| while (ph[i + 1].checker != ngx_http_core_try_files_phase && |
| ph[i + 1].checker != ngx_http_core_content_phase) { |
| ph[i] = ph[i + 1]; |
| ph[i].next--; |
| i++; |
| } |
| |
| // insert ps phase handler |
| ph[i].checker = ps_phase_handler; |
| ph[i].handler = NULL; |
| ph[i].next = i + 1; |
| |
| // next preaccess handler |
| r->phase_handler--; |
| return NGX_DECLINED; |
| } |
| |
| ngx_int_t ps_etag_filter_init(ngx_conf_t* cf) { |
| ps_main_conf_t* cfg_m = static_cast<ps_main_conf_t*>( |
| ngx_http_conf_get_module_main_conf(cf, ngx_pagespeed)); |
| if (cfg_m->driver_factory != NULL) { |
| ngx_http_ef_next_header_filter = ngx_http_top_header_filter; |
| ngx_http_top_header_filter = ps_etag_header_filter; |
| } |
| return NGX_OK; |
| } |
| |
| // Called before configuration. |
| ngx_int_t ps_pre_init(ngx_conf_t* cf) { |
| // Setup an intervention setter for gzip configuration and check |
| // gzip configuration command signatures. |
| g_gzip_setter.Init(cf); |
| return NGX_OK; |
| } |
| |
| ngx_int_t ps_init(ngx_conf_t* cf) { |
| // Only put register pagespeed code to run if there was a "pagespeed" |
| // configuration option set in the config file. With "pagespeed off" we |
| // consider every request and choose not to do anything, while with no |
| // "pagespeed" directives we won't have any effect after nginx is done loading |
| // its configuration. |
| |
| ps_main_conf_t* cfg_m = static_cast<ps_main_conf_t*>( |
| ngx_http_conf_get_module_main_conf(cf, ngx_pagespeed)); |
| |
| // The driver factory is on the main config and is non-NULL iff there is a |
| // pagespeed configuration option in the main config or a server block. Note |
| // that if any server block has pagespeed 'on' then our header filter, body |
| // filter, and content handler will run in every server block. This is ok, |
| // because they will notice that the server context is NULL and do nothing. |
| if (cfg_m->driver_factory != NULL) { |
| // The filter init order is important. |
| ps_in_place_filter_init(); |
| |
| ps_html_rewrite_fix_headers_filter_init(); |
| ps_base_fetch_filter_init(); |
| ps_html_rewrite_filter_init(); |
| |
| ngx_http_core_main_conf_t* cmcf = static_cast<ngx_http_core_main_conf_t*>( |
| ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module)); |
| |
| ngx_http_handler_pt* h = static_cast<ngx_http_handler_pt*>( |
| ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers)); |
| if (h == NULL) { |
| return NGX_ERROR; |
| } |
| *h = ps_preaccess_handler; |
| } |
| |
| return NGX_OK; |
| } |
| |
| ngx_http_module_t ps_etag_filter_module = { |
| NULL, // preconfiguration |
| ps_etag_filter_init, // postconfiguration |
| NULL, |
| NULL, // initialize main configuration |
| NULL, |
| NULL, |
| NULL, |
| NULL |
| }; |
| |
| ngx_http_module_t ps_module = { |
| ps_pre_init, // preconfiguration |
| ps_init, // postconfiguration |
| |
| ps_create_main_conf, |
| NULL, // initialize main configuration |
| |
| ps_create_srv_conf, |
| ps_merge_srv_conf, |
| |
| ps_create_loc_conf, |
| ps_merge_loc_conf |
| }; |
| |
| // called after configuration is complete, but before nginx starts forking |
| ngx_int_t ps_init_module(ngx_cycle_t* cycle) { |
| ps_main_conf_t* cfg_m = static_cast<ps_main_conf_t*>( |
| ngx_http_cycle_get_module_main_conf(cycle, ngx_pagespeed)); |
| |
| ngx_http_core_main_conf_t* cmcf = static_cast<ngx_http_core_main_conf_t*>( |
| ngx_http_cycle_get_module_main_conf(cycle, ngx_http_core_module)); |
| ngx_http_core_srv_conf_t** cscfp = static_cast<ngx_http_core_srv_conf_t**>( |
| cmcf->servers.elts); |
| ngx_uint_t s; |
| |
| std::vector<SystemServerContext*> server_contexts; |
| // Iterate over all configured server{} blocks to collect the server contexts. |
| for (s = 0; s < cmcf->servers.nelts; s++) { |
| ps_srv_conf_t* cfg_s = static_cast<ps_srv_conf_t*>( |
| cscfp[s]->ctx->srv_conf[ngx_pagespeed.ctx_index]); |
| if (cfg_s->server_context != NULL) { |
| server_contexts.push_back(cfg_s->server_context); |
| } |
| } |
| |
| GoogleString error_message; |
| int error_index = -1; |
| Statistics* global_statistics = NULL; |
| cfg_m->driver_factory->PostConfig( |
| server_contexts, &error_message, &error_index, &global_statistics); |
| if (error_index != -1) { |
| server_contexts[error_index]->message_handler()->Message( |
| kError, "ngx_pagespeed is enabled. %s", error_message.c_str()); |
| return NGX_ERROR; |
| } |
| |
| if (!server_contexts.empty()) { |
| // TODO(oschaaf): this ignores sigpipe messages from memcached. |
| // however, it would be better to not have those signals generated |
| // in the first place, as suppressing them this way may interfere |
| // with other modules that actually are interested in these signals |
| ps_ignore_sigpipe(); |
| |
| // If no shared-mem statistics are enabled, then init using the default |
| // NullStatistics. |
| if (global_statistics == NULL) { |
| NgxRewriteDriverFactory::InitStats(cfg_m->driver_factory->statistics()); |
| } |
| |
| ngx_http_core_loc_conf_t* clcf = static_cast<ngx_http_core_loc_conf_t*>( |
| ngx_http_conf_get_module_loc_conf((*cscfp), ngx_http_core_module)); |
| |
| cfg_m->driver_factory->set_resolver(clcf->resolver); |
| cfg_m->driver_factory->set_resolver_timeout(clcf->resolver_timeout); |
| |
| if (!cfg_m->driver_factory->CheckResolver()) { |
| cfg_m->handler->Message( |
| kError, |
| "UseNativeFetcher is on, please configure a resolver."); |
| return NGX_ERROR; |
| } |
| // Update logging to the configured error_log in the http{} block. |
| cfg_m->driver_factory->LoggingInit(cycle->log); |
| cfg_m->driver_factory->RootInit(); |
| } else { |
| if (!factory_init_called) { |
| cfg_m->driver_factory->LoggingInit(cycle->log); |
| cfg_m->driver_factory->Init(); |
| factory_init_called = true; |
| } |
| delete cfg_m->driver_factory; |
| cfg_m->driver_factory = NULL; |
| } |
| return NGX_OK; |
| } |
| |
| // Called when nginx forks worker processes. No threads should be started |
| // before this. |
| ngx_int_t ps_init_child_process(ngx_cycle_t* cycle) { |
| ps_main_conf_t* cfg_m = static_cast<ps_main_conf_t*>( |
| ngx_http_cycle_get_module_main_conf(cycle, ngx_pagespeed)); |
| if (cfg_m->driver_factory == NULL) { |
| return NGX_OK; |
| } |
| |
| SystemRewriteDriverFactory::InitApr(); |
| |
| // ChildInit() will initialise all ServerContexts, which we need to |
| // create ProxyFetchFactories below |
| cfg_m->driver_factory->LoggingInit(cycle->log); |
| cfg_m->driver_factory->ChildInit(); |
| |
| ngx_http_core_main_conf_t* cmcf = static_cast<ngx_http_core_main_conf_t*>( |
| ngx_http_cycle_get_module_main_conf(cycle, ngx_http_core_module)); |
| ngx_http_core_srv_conf_t** cscfp = static_cast<ngx_http_core_srv_conf_t**>( |
| cmcf->servers.elts); |
| ngx_uint_t s; |
| |
| // Iterate over all configured server{} blocks, and find our context in it, |
| // so we can create and set a ProxyFetchFactory for it. |
| for (s = 0; s < cmcf->servers.nelts; s++) { |
| ps_srv_conf_t* cfg_s = static_cast<ps_srv_conf_t*>( |
| cscfp[s]->ctx->srv_conf[ngx_pagespeed.ctx_index]); |
| // Some server{} blocks may not have a ServerContext in that case we must |
| // not instantiate a ProxyFetchFactory. |
| if (cfg_s->server_context != NULL) { |
| ngx_http_core_loc_conf_t* clcf = static_cast<ngx_http_core_loc_conf_t*>( |
| cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index]); |
| cfg_m->driver_factory->SetServerContextMessageHandler( |
| cfg_s->server_context, clcf->error_log); |
| cfg_s->proxy_fetch_factory = new ProxyFetchFactory(cfg_s->server_context); |
| } |
| } |
| |
| if (!cfg_m->driver_factory->InitNgxUrlAsyncFetchers()) { |
| return NGX_ERROR; |
| } |
| cfg_m->driver_factory->StartThreads(); |
| |
| return NGX_OK; |
| } |
| |
| } // namespace |
| |
| } // namespace net_instaweb |
| |
| ngx_module_t ngx_pagespeed_etag_filter = { |
| NGX_MODULE_V1, |
| &net_instaweb::ps_etag_filter_module, |
| NULL, |
| NGX_HTTP_MODULE, |
| NULL, |
| NULL, |
| NULL, |
| NULL, |
| NULL, |
| NULL, |
| NULL, |
| NGX_MODULE_V1_PADDING |
| }; |
| |
| ngx_module_t ngx_pagespeed = { |
| NGX_MODULE_V1, |
| &net_instaweb::ps_module, |
| net_instaweb::ps_commands, |
| NGX_HTTP_MODULE, |
| NULL, |
| net_instaweb::ps_init_module, |
| net_instaweb::ps_init_child_process, |
| NULL, |
| NULL, |
| NULL, |
| NULL, |
| NGX_MODULE_V1_PADDING |
| }; |