blob: ba056519dc3f9e60b476ac005df2bfb11592f1f2 [file] [log] [blame]
/*
* Copyright 2015 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: jmarantz@google.com (Joshua Marantz)
#include "net/instaweb/rewriter/public/iframe_fetcher.h"
#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/rewriter/public/domain_lawyer.h"
#include "net/instaweb/rewriter/public/domain_rewrite_filter.h"
#include "net/instaweb/rewriter/public/mobilize_rewrite_filter.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/html/html_keywords.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/http/user_agent_matcher.h"
namespace net_instaweb {
const char IframeFetcher::kIframeId[] = "psmob-iframe";
namespace {
bool MustProxyFetch(const GoogleUrl& gurl) {
StringPiece path = gurl.PathSansQuery();
return ((path == "/favicon.ico") || (path == "/robots.txt"));
}
} // namespace
IframeFetcher::IframeFetcher(const RewriteOptions* options,
const UserAgentMatcher* matcher,
UrlAsyncFetcher* proxy_fetcher)
: options_(options),
user_agent_matcher_(matcher),
proxy_fetcher_(proxy_fetcher) {
// This normally gets called in the HtmlParse constructor, but since
// that doesn't get called here we need to initialize it ourselves.
HtmlKeywords::Init();
}
IframeFetcher::~IframeFetcher() {
}
void IframeFetcher::Fetch(const GoogleString& url,
MessageHandler* message_handler,
AsyncFetch* fetch) {
// It's bad to serve some resources as an HTML iframe response,
// so proxy them.
GoogleUrl gurl(url);
if (!gurl.IsWebValid() || MustProxyFetch(gurl)) {
proxy_fetcher_->Fetch(url, message_handler, fetch);
return;
}
GoogleString mapped_url, host_header;
const DomainLawyer* lawyer = options_->domain_lawyer();
bool mapped_to_self = false;
if (lawyer->proxy_suffix().empty()) {
bool is_proxy;
if (!options_->domain_lawyer()->MapOrigin(
url, &mapped_url, &host_header, &is_proxy)) {
mapped_to_self = true;
}
} else {
GoogleString origin_host;
if (!lawyer->StripProxySuffix(gurl, &mapped_url, &origin_host)) {
mapped_to_self = true;
}
}
GoogleString escaped_url;
HtmlKeywords::Escape(mapped_url, &escaped_url);
fetch->response_headers()->Add(HttpAttributes::kContentType, "text/html");
const char* user_agent = fetch->request_headers()->Lookup1(
HttpAttributes::kUserAgent);
if (mapped_to_self || (mapped_url == url)) {
// We would cause a redirect loop or an iframe-loop if we allow this to
// happen, so just fail.
RespondWithError(escaped_url, fetch, message_handler);
} else if (!options_->mob_iframe_disable() &&
(user_agent != NULL) &&
SupportedDevice(user_agent) &&
MobilizeRewriteFilter::IsApplicableFor(options_, user_agent,
user_agent_matcher_) &&
/* Note: we will turn off mobilize in noscript mode, where we
want to redirect, too, since the iframe relies on a script
TODO(morlovich): May be cleaner to have a "in noscript mode"
predicate instead.
*/
options_->Enabled(RewriteOptions::kMobilize)) {
RespondWithIframe(escaped_url, fetch, message_handler);
} else {
RespondWithRedirect(mapped_url, escaped_url, fetch, message_handler);
}
fetch->Done(true);
}
bool IframeFetcher::SupportedDevice(const char* user_agent) const {
return user_agent_matcher_->SupportsMobilization(user_agent);
}
void IframeFetcher::RespondWithIframe(const GoogleString& escaped_url,
AsyncFetch* fetch,
MessageHandler* message_handler) {
fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK);
// The viewport should be configured to match the viewport of the page being
// iframed.
GoogleString viewport;
GoogleString scrolling_attribute;
if (options_->mob_iframe_viewport() != "none") {
GoogleString escaped_viewport_content;
HtmlKeywords::Escape(options_->mob_iframe_viewport(),
&escaped_viewport_content);
viewport = StrCat("<meta name=\"viewport\" content=\"",
escaped_viewport_content, "\">");
const char* user_agent =
fetch->request_headers()->Lookup1(HttpAttributes::kUserAgent);
if (user_agent_matcher_->IsiOSUserAgent(user_agent)) {
// Setting scrolling="no" on the iframe keeps the iframe from expanding to
// be too large on iOS devices.
scrolling_attribute = " scrolling=\"no\"";
}
}
GoogleString head =
StrCat("<head><link rel=\"canonical\" href=\"", escaped_url,
"\"><meta charset=\"utf-8\">", viewport, "</head>");
GoogleString body = StrCat(
"<body class=\"mob-iframe\">"
"<div id=\"psmob-iframe-container\">"
"<div id=\"psmob-spacer\"></div>"
"<iframe id=\"",
kIframeId, "\" src=\"", escaped_url, "\"", scrolling_attribute,
"></iframe></div></body>");
// Avoid quirks-mode by specifying an HTML doctype.
fetch->Write(StrCat("<!DOCTYPE html><html>", head, body, "</html>"),
message_handler);
}
void IframeFetcher::RespondWithRedirect(const GoogleString& url,
const GoogleString& escaped_url,
AsyncFetch* fetch,
MessageHandler* message_handler) {
ResponseHeaders* response = fetch->response_headers();
response->SetStatusAndReason(HttpStatus::kTemporaryRedirect);
response->Add(HttpAttributes::kLocation, url);
response->Add(DomainRewriteFilter::kStickyRedirectHeader, "on");
response->Add(HttpAttributes::kCacheControl, "private, max-age=0");
fetch->Write(StrCat("<html><body>Redirecting to ", escaped_url,
"</body></html>"), message_handler);
}
void IframeFetcher::RespondWithError(const GoogleString& escaped_url,
AsyncFetch* fetch,
MessageHandler* message_handler) {
ResponseHeaders* response = fetch->response_headers();
response->SetStatusAndReason(HttpStatus::kNotImplemented);
fetch->Write(StrCat("<html><body>Error: redirecting to ", escaped_url,
" would cause a recirect loop.</body></html>"),
message_handler);
}
} // namespace net_instaweb