// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome_frame/urlmon_url_request.h"
#include <urlmon.h>
#include <wininet.h>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread.h"
#include "chrome/common/automation_messages.h"
#include "chrome_frame/bind_context_info.h"
#include "chrome_frame/chrome_frame_activex_base.h"
#include "chrome_frame/extra_system_apis.h"
#include "chrome_frame/html_utils.h"
#include "chrome_frame/urlmon_upload_data_stream.h"
#include "chrome_frame/urlmon_url_request_private.h"
#include "chrome_frame/utils.h"
#include "net/base/load_flags.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#define IS_HTTP_SUCCESS_CODE(code) (code >= 200 && code <= 299)
UrlmonUrlRequest::UrlmonUrlRequest()
: pending_read_size_(0),
headers_received_(false),
calling_delegate_(0),
thread_(NULL),
parent_window_(NULL),
privileged_mode_(false),
pending_(false),
is_expecting_download_(true),
cleanup_transaction_(false) {
DVLOG(1) << __FUNCTION__ << me();
}
UrlmonUrlRequest::~UrlmonUrlRequest() {
DVLOG(1) << __FUNCTION__ << me();
}
std::string UrlmonUrlRequest::me() const {
return base::StringPrintf(" id: %i Obj: %X ", id(), this);
}
bool UrlmonUrlRequest::Start() {
DVLOG(1) << __FUNCTION__ << me() << url();
DCHECK(thread_ == 0 || thread_ == base::PlatformThread::CurrentId());
thread_ = base::PlatformThread::CurrentId();
status_.Start();
// Initialize the net::HostPortPair structure from the url initially. We may
// not receive the ip address of the host if the request is satisfied from
// the cache.
socket_address_ = net::HostPortPair::FromURL(GURL(url()));
// The UrlmonUrlRequest instance can get destroyed in the context of
// StartAsyncDownload if BindToStorage finishes synchronously with an error.
// Grab a reference to protect against this.
scoped_refptr<UrlmonUrlRequest> ref(this);
HRESULT hr = StartAsyncDownload();
if (FAILED(hr) && status_.get_state() != UrlmonUrlRequest::Status::DONE) {
status_.Done();
status_.set_result(net::URLRequestStatus::FAILED, HresultToNetError(hr));
NotifyDelegateAndDie();
}
return true;
}
void UrlmonUrlRequest::Stop() {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
DCHECK((status_.get_state() != Status::DONE) == (binding_ != NULL));
Status::State state = status_.get_state();
delegate_ = NULL;
// If DownloadInHost is already requested, we will quit soon anyway.
if (terminate_requested())
return;
switch (state) {
case Status::WORKING:
status_.Cancel();
if (binding_)
binding_->Abort();
break;
case Status::ABORTING:
status_.Cancel();
break;
case Status::DONE:
status_.Cancel();
NotifyDelegateAndDie();
break;
}
}
bool UrlmonUrlRequest::Read(int bytes_to_read) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
DCHECK_GE(bytes_to_read, 0);
DCHECK_EQ(0, calling_delegate_);
DVLOG(1) << __FUNCTION__ << me();
is_expecting_download_ = false;
// Re-entrancy check. Thou shall not call Read() while process OnReadComplete!
DCHECK_EQ(0u, pending_read_size_);
if (pending_read_size_ != 0)
return false;
DCHECK((status_.get_state() != Status::DONE) == (binding_ != NULL));
if (status_.get_state() == Status::ABORTING)
return true;
// Send data if available.
size_t bytes_copied = 0;
if ((bytes_copied = SendDataToDelegate(bytes_to_read))) {
DVLOG(1) << __FUNCTION__ << me() << " bytes read: " << bytes_copied;
return true;
}
if (status_.get_state() == Status::WORKING) {
DVLOG(1) << __FUNCTION__ << me() << " pending: " << bytes_to_read;
pending_read_size_ = bytes_to_read;
} else {
DVLOG(1) << __FUNCTION__ << me() << " Response finished.";
NotifyDelegateAndDie();
}
return true;
}
HRESULT UrlmonUrlRequest::InitPending(const GURL& url, IMoniker* moniker,
IBindCtx* bind_context,
bool enable_frame_busting,
bool privileged_mode,
HWND notification_window,
IStream* cache) {
DVLOG(1) << __FUNCTION__ << me() << url.spec();
DCHECK(bind_context_ == NULL);
DCHECK(moniker_ == NULL);
DCHECK(cache_ == NULL);
DCHECK(thread_ == 0 || thread_ == base::PlatformThread::CurrentId());
thread_ = base::PlatformThread::CurrentId();
bind_context_ = bind_context;
moniker_ = moniker;
enable_frame_busting_ = enable_frame_busting;
privileged_mode_ = privileged_mode;
parent_window_ = notification_window;
cache_ = cache;
set_url(url.spec());
set_pending(true);
// Request has already started and data is fetched. We will get the
// GetBindInfo call as per contract but the return values are
// ignored. So just set "get" as a method to make our GetBindInfo
// implementation happy.
method_ = "get";
return S_OK;
}
void UrlmonUrlRequest::TerminateBind(const TerminateBindCallback& callback) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
DVLOG(1) << __FUNCTION__ << me();
cleanup_transaction_ = false;
if (status_.get_state() == Status::DONE) {
// Binding is stopped. Note result could be an error.
callback.Run(moniker_, bind_context_, upload_data_,
request_headers_.c_str());
} else {
// WORKING (ABORTING?). Save the callback.
// Now we will return INET_TERMINATE_BIND from ::OnDataAvailable() and in
// ::OnStopBinding will invoke the callback passing our moniker and
// bind context.
terminate_bind_callback_ = callback;
if (pending_data_) {
// For downloads to work correctly, we must induce a call to
// OnDataAvailable so that we can download INET_E_TERMINATED_BIND and
// get IE into the correct state.
// To accomplish this we read everything that's readily available in
// the current stream. Once we've reached the end of the stream we
// should get E_PENDING back and then later we'll get that call
// to OnDataAvailable.
std::string data;
base::win::ScopedComPtr<IStream> read_stream(pending_data_);
HRESULT hr;
while ((hr = ReadStream(read_stream, 0xffff, &data)) == S_OK) {
// Just drop the data.
}
DLOG_IF(WARNING, hr != E_PENDING) << __FUNCTION__ <<
base::StringPrintf(" expected E_PENDING but got 0x%08X", hr);
}
}
}
size_t UrlmonUrlRequest::SendDataToDelegate(size_t bytes_to_read) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
DCHECK_NE(id(), -1);
DCHECK_GT(bytes_to_read, 0U);
size_t bytes_copied = 0;
if (delegate_) {
std::string read_data;
if (cache_) {
HRESULT hr = ReadStream(cache_, bytes_to_read, &read_data);
if (hr == S_FALSE || read_data.length() < bytes_to_read) {
DVLOG(1) << __FUNCTION__ << me() << "all cached data read";
cache_.Release();
}
}
if (read_data.empty() && pending_data_) {
size_t pending_data_read_save = pending_read_size_;
pending_read_size_ = 0;
// AddRef the stream while we call Read to avoid a potential issue
// where we can get a call to OnDataAvailable while inside Read and
// in our OnDataAvailable call, we can release the stream object
// while still using it.
base::win::ScopedComPtr<IStream> pending(pending_data_);
HRESULT hr = ReadStream(pending, bytes_to_read, &read_data);
if (read_data.empty())
pending_read_size_ = pending_data_read_save;
// If we received S_FALSE it indicates that there is no more data in the
// stream. Clear it to ensure that OnStopBinding correctly sends over the
// response end notification to chrome.
if (hr == S_FALSE)
pending_data_.Release();
}
bytes_copied = read_data.length();
if (bytes_copied) {
++calling_delegate_;
DCHECK_NE(id(), -1);
// The delegate can go away in the middle of ReadStream
if (delegate_)
delegate_->OnReadComplete(id(), read_data);
--calling_delegate_;
}
} else {
DLOG(ERROR) << __FUNCTION__ << me() << "no delegate";
}
return bytes_copied;
}
STDMETHODIMP UrlmonUrlRequest::OnStartBinding(DWORD reserved,
IBinding* binding) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
binding_ = binding;
if (pending_) {
response_headers_ = GetHttpHeadersFromBinding(binding_);
DCHECK(!response_headers_.empty());
}
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::GetPriority(LONG *priority) {
if (!priority)
return E_POINTER;
*priority = THREAD_PRIORITY_NORMAL;
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::OnLowResource(DWORD reserved) {
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::OnProgress(ULONG progress, ULONG max_progress,
ULONG status_code, LPCWSTR status_text) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
if (status_.get_state() != Status::WORKING)
return S_OK;
// Ignore any notifications received while we are in the pending state
// waiting for the request to be initiated by Chrome.
if (pending_ && status_code != BINDSTATUS_REDIRECTING)
return S_OK;
if (!delegate_) {
DVLOG(1) << "Invalid delegate";
return S_OK;
}
switch (status_code) {
case BINDSTATUS_CONNECTING: {
if (status_text) {
socket_address_.set_host(WideToUTF8(status_text));
}
break;
}
case BINDSTATUS_REDIRECTING: {
// If we receive a redirect for the initial pending request initiated
// when our document loads we should stash it away and inform Chrome
// accordingly when it requests data for the original URL.
base::win::ScopedComPtr<BindContextInfo> info;
BindContextInfo::FromBindContext(bind_context_, info.Receive());
DCHECK(info);
GURL previously_redirected(info ? info->GetUrl() : std::wstring());
if (GURL(status_text) != previously_redirected) {
DVLOG(1) << __FUNCTION__ << me() << "redirect from " << url()
<< " to " << status_text;
// Fetch the redirect status as they aren't all equal (307 in particular
// retains the HTTP request verb).
int http_code = GetHttpResponseStatusFromBinding(binding_);
status_.SetRedirected(http_code, WideToUTF8(status_text));
// Abort. We will inform Chrome in OnStopBinding callback.
binding_->Abort();
return E_ABORT;
}
break;
}
case BINDSTATUS_COOKIE_SENT:
delegate_->AddPrivacyDataForUrl(url(), "", COOKIEACTION_READ);
break;
case BINDSTATUS_COOKIE_SUPPRESSED:
delegate_->AddPrivacyDataForUrl(url(), "", COOKIEACTION_SUPPRESS);
break;
case BINDSTATUS_COOKIE_STATE_ACCEPT:
delegate_->AddPrivacyDataForUrl(url(), "", COOKIEACTION_ACCEPT);
break;
case BINDSTATUS_COOKIE_STATE_REJECT:
delegate_->AddPrivacyDataForUrl(url(), "", COOKIEACTION_REJECT);
break;
case BINDSTATUS_COOKIE_STATE_LEASH:
delegate_->AddPrivacyDataForUrl(url(), "", COOKIEACTION_LEASH);
break;
case BINDSTATUS_COOKIE_STATE_DOWNGRADE:
delegate_->AddPrivacyDataForUrl(url(), "", COOKIEACTION_DOWNGRADE);
break;
case BINDSTATUS_COOKIE_STATE_UNKNOWN:
NOTREACHED() << L"Unknown cookie state received";
break;
default:
DVLOG(1) << __FUNCTION__ << me()
<< base::StringPrintf(L"code: %i status: %ls", status_code,
status_text);
break;
}
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::OnStopBinding(HRESULT result, LPCWSTR error) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
DVLOG(1) << __FUNCTION__ << me()
<< "- Request stopped, Result: " << std::hex << result;
DCHECK(status_.get_state() == Status::WORKING ||
status_.get_state() == Status::ABORTING);
Status::State state = status_.get_state();
// Mark we a are done.
status_.Done();
if (result == INET_E_TERMINATED_BIND) {
if (terminate_requested()) {
terminate_bind_callback_.Run(moniker_, bind_context_, upload_data_,
request_headers_.c_str());
} else {
cleanup_transaction_ = true;
}
// We may have returned INET_E_TERMINATED_BIND from OnDataAvailable.
result = S_OK;
}
if (state == Status::WORKING) {
status_.set_result(result);
if (FAILED(result)) {
int http_code = GetHttpResponseStatusFromBinding(binding_);
// For certain requests like empty POST requests the server can return
// back a HTTP success code in the range 200 to 299. We need to flag
// these requests as succeeded.
if (IS_HTTP_SUCCESS_CODE(http_code)) {
// If this DCHECK fires it means that the server returned a HTTP
// success code outside the standard range 200-206. We need to confirm
// if the following code path is correct.
DCHECK_LE(http_code, 206);
status_.set_result(S_OK);
std::string headers = GetHttpHeadersFromBinding(binding_);
OnResponse(0, UTF8ToWide(headers).c_str(), NULL, NULL);
} else if (net::HttpResponseHeaders::IsRedirectResponseCode(http_code) &&
result == E_ACCESSDENIED) {
// Special case. If the last request was a redirect and the current OS
// error value is E_ACCESSDENIED, that means an unsafe redirect was
// attempted. In that case, correct the OS error value to be the more
// specific ERR_UNSAFE_REDIRECT error value.
status_.set_result(net::URLRequestStatus::FAILED,
net::ERR_UNSAFE_REDIRECT);
}
}
// The code below seems easy but it is not. :)
// The network policy in Chrome network is that error code/end_of_stream
// should be returned only as a result of read (or start) request.
// Here are the possible cases:
// pending_data_|pending_read
// FALSE |FALSE => EndRequest if no headers, otherwise wait for Read.
// FALSE |TRUE => EndRequest.
// TRUE |FALSE => Wait for Read.
// TRUE |TRUE => Something went wrong!!
if (pending_data_) {
DCHECK_EQ(pending_read_size_, 0UL);
ReleaseBindings();
return S_OK;
}
if (headers_received_ && pending_read_size_ == 0) {
ReleaseBindings();
return S_OK;
}
// No headers or there is a pending read from Chrome.
NotifyDelegateAndDie();
return S_OK;
}
// Status::ABORTING
if (status_.was_redirected()) {
// Just release bindings here. Chrome will issue EndRequest(request_id)
// after processing headers we had provided.
if (!pending_) {
std::string headers = GetHttpHeadersFromBinding(binding_);
OnResponse(0, UTF8ToWide(headers).c_str(), NULL, NULL);
}
ReleaseBindings();
return S_OK;
}
// Stop invoked.
NotifyDelegateAndDie();
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::GetBindInfo(DWORD* bind_flags,
BINDINFO* bind_info) {
if ((bind_info == NULL) || (bind_info->cbSize == 0) || (bind_flags == NULL))
return E_INVALIDARG;
*bind_flags = BINDF_ASYNCHRONOUS | BINDF_ASYNCSTORAGE | BINDF_PULLDATA;
bind_info->dwOptionsFlags = INTERNET_FLAG_NO_AUTO_REDIRECT;
bind_info->dwOptions = BINDINFO_OPTIONS_WININETFLAG;
// TODO(ananta)
// Look into whether the other load flags need to be supported in chrome
// frame.
if (load_flags_ & net::LOAD_VALIDATE_CACHE)
*bind_flags |= BINDF_RESYNCHRONIZE;
if (load_flags_ & net::LOAD_BYPASS_CACHE)
*bind_flags |= BINDF_GETNEWESTVERSION;
if (LowerCaseEqualsASCII(method(), "get")) {
bind_info->dwBindVerb = BINDVERB_GET;
} else if (LowerCaseEqualsASCII(method(), "post")) {
bind_info->dwBindVerb = BINDVERB_POST;
} else if (LowerCaseEqualsASCII(method(), "put")) {
bind_info->dwBindVerb = BINDVERB_PUT;
} else {
std::wstring verb(ASCIIToWide(StringToUpperASCII(method())));
bind_info->dwBindVerb = BINDVERB_CUSTOM;
bind_info->szCustomVerb = reinterpret_cast<wchar_t*>(
::CoTaskMemAlloc((verb.length() + 1) * sizeof(wchar_t)));
lstrcpyW(bind_info->szCustomVerb, verb.c_str());
}
if (bind_info->dwBindVerb == BINDVERB_POST ||
bind_info->dwBindVerb == BINDVERB_PUT ||
post_data_len() > 0) {
// Bypass caching proxies on upload requests and avoid writing responses to
// the browser's cache.
*bind_flags |= BINDF_GETNEWESTVERSION | BINDF_PRAGMA_NO_CACHE;
// Attempt to avoid storing the response for upload requests.
// See http://crbug.com/55918
if (resource_type_ != ResourceType::MAIN_FRAME)
*bind_flags |= BINDF_NOWRITECACHE;
// Initialize the STGMEDIUM.
memset(&bind_info->stgmedData, 0, sizeof(STGMEDIUM));
bind_info->grfBindInfoF = 0;
if (bind_info->dwBindVerb != BINDVERB_CUSTOM)
bind_info->szCustomVerb = NULL;
if ((post_data_len() || is_chunked_upload()) &&
get_upload_data(&bind_info->stgmedData.pstm) == S_OK) {
bind_info->stgmedData.tymed = TYMED_ISTREAM;
if (!is_chunked_upload()) {
bind_info->cbstgmedData = static_cast<DWORD>(post_data_len());
}
DVLOG(1) << __FUNCTION__ << me() << method()
<< " request with " << base::Int64ToString(post_data_len())
<< " bytes. url=" << url();
} else {
DVLOG(1) << __FUNCTION__ << me() << "POST request with no data!";
}
}
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::OnDataAvailable(DWORD flags, DWORD size,
FORMATETC* formatetc,
STGMEDIUM* storage) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
DVLOG(1) << __FUNCTION__ << me() << "bytes available: " << size;
if (terminate_requested()) {
DVLOG(1) << " Download requested. INET_E_TERMINATED_BIND returned";
return INET_E_TERMINATED_BIND;
}
if (!storage || (storage->tymed != TYMED_ISTREAM)) {
NOTREACHED();
return E_INVALIDARG;
}
IStream* read_stream = storage->pstm;
if (!read_stream) {
NOTREACHED();
return E_UNEXPECTED;
}
// Some requests such as HEAD have zero data.
if (size > 0)
pending_data_ = read_stream;
if (pending_read_size_) {
size_t bytes_copied = SendDataToDelegate(pending_read_size_);
DVLOG(1) << __FUNCTION__ << me() << "size read: " << bytes_copied;
} else {
DVLOG(1) << __FUNCTION__ << me() << "- waiting for remote read";
}
if (BSCF_LASTDATANOTIFICATION & flags) {
if (!is_expecting_download_ || pending()) {
DVLOG(1) << __FUNCTION__ << me() << "EOF";
return S_OK;
}
// Always return INET_E_TERMINATED_BIND to allow bind context reuse
// if DownloadToHost is suddenly requested.
DVLOG(1) << __FUNCTION__ << " EOF: INET_E_TERMINATED_BIND returned";
return INET_E_TERMINATED_BIND;
}
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::OnObjectAvailable(REFIID iid, IUnknown* object) {
// We are calling BindToStorage on the moniker we should always get called
// back on OnDataAvailable and should never get OnObjectAvailable
NOTREACHED();
return E_NOTIMPL;
}
STDMETHODIMP UrlmonUrlRequest::BeginningTransaction(const wchar_t* url,
const wchar_t* current_headers, DWORD reserved,
wchar_t** additional_headers) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
if (!additional_headers) {
NOTREACHED();
return E_POINTER;
}
DVLOG(1) << __FUNCTION__ << me() << "headers: \n" << current_headers;
if (status_.get_state() == Status::ABORTING) {
// At times the BINDSTATUS_REDIRECTING notification which is sent to the
// IBindStatusCallback interface does not have an accompanying HTTP
// redirect status code, i.e. the attempt to query the HTTP status code
// from the binding returns 0, 200, etc which are invalid redirect codes.
// We don't want urlmon to follow redirects. We return E_ABORT in our
// IBindStatusCallback::OnProgress function and also abort the binding.
// However urlmon still tries to establish a transaction with the
// redirected URL which confuses the web server.
// Fix is to abort the attempted transaction.
DLOG(WARNING) << __FUNCTION__ << me()
<< ": Aborting connection to URL:"
<< url
<< " as the binding has been aborted";
return E_ABORT;
}
HRESULT hr = S_OK;
std::string new_headers;
if (is_chunked_upload()) {
new_headers = base::StringPrintf("Transfer-Encoding: chunked\r\n");
}
if (!extra_headers().empty()) {
// TODO(robertshield): We may need to sanitize headers on POST here.
new_headers += extra_headers();
}
if (!referrer().empty()) {
// Referrer is famously misspelled in HTTP:
new_headers += base::StringPrintf("Referer: %s\r\n", referrer().c_str());
}
// In the rare case if "User-Agent" string is already in |current_headers|.
// We send Chrome's user agent in requests initiated within ChromeFrame to
// enable third party content in pages rendered in ChromeFrame to correctly
// send content for Chrome as the third party content may not be equipped to
// identify chromeframe as the user agent. This also ensures that the user
// agent reported in scripts in chrome frame is consistent with that sent
// in outgoing requests.
std::string user_agent = http_utils::AddChromeFrameToUserAgentValue(
http_utils::GetChromeUserAgent());
new_headers += ReplaceOrAddUserAgent(current_headers, user_agent);
if (!new_headers.empty()) {
*additional_headers = reinterpret_cast<wchar_t*>(
CoTaskMemAlloc((new_headers.size() + 1) * sizeof(wchar_t)));
if (*additional_headers == NULL) {
NOTREACHED();
hr = E_OUTOFMEMORY;
} else {
lstrcpynW(*additional_headers, ASCIIToWide(new_headers).c_str(),
new_headers.size());
}
}
request_headers_ = new_headers;
return hr;
}
STDMETHODIMP UrlmonUrlRequest::OnResponse(DWORD dwResponseCode,
const wchar_t* response_headers, const wchar_t* request_headers,
wchar_t** additional_headers) {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
DVLOG(1) << __FUNCTION__ << me() << "headers: \n"
<< (response_headers == NULL ? L"EMPTY" : response_headers);
if (!delegate_) {
DLOG(WARNING) << "Invalid delegate";
return S_OK;
}
delegate_->AddPrivacyDataForUrl(url(), "", 0);
std::string raw_headers;
if (response_headers)
raw_headers = WideToUTF8(response_headers);
// Security check for frame busting headers. We don't honor the headers
// as-such, but instead simply kill requests which we've been asked to
// look for if they specify a value for "X-Frame-Options" other than
// "ALLOWALL" (the others are "deny" and "sameorigin"). This puts the onus
// on the user of the UrlRequest to specify whether or not requests should
// be inspected. For ActiveDocuments, the answer is "no", since WebKit's
// detection/handling is sufficient and since ActiveDocuments cannot be
// hosted as iframes. For NPAPI and ActiveX documents, the Initialize()
// function of the PluginUrlRequest object allows them to specify how they'd
// like requests handled. Both should set enable_frame_busting_ to true to
// avoid CSRF attacks. Should WebKit's handling of this ever change, we will
// need to re-visit how and when frames are killed to better mirror a policy
// which may do something other than kill the sub-document outright.
// NOTE(slightlyoff): We don't use net::HttpResponseHeaders here because
// of lingering ICU/base_noicu issues.
if (enable_frame_busting_) {
if (http_utils::HasFrameBustingHeader(raw_headers)) {
DLOG(ERROR) << "X-Frame-Options header other than ALLOWALL " <<
"detected, navigation canceled";
return E_FAIL;
}
}
DVLOG(1) << __FUNCTION__ << me() << "Calling OnResponseStarted";
// Inform the delegate.
headers_received_ = true;
DCHECK_NE(id(), -1);
delegate_->OnResponseStarted(id(),
"", // mime_type
raw_headers.c_str(), // headers
0, // size
base::Time(), // last_modified
status_.get_redirection().utf8_url,
status_.get_redirection().http_code,
socket_address_,
post_data_len());
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::GetWindow(const GUID& guid_reason,
HWND* parent_window) {
if (!parent_window)
return E_INVALIDARG;
#ifndef NDEBUG
wchar_t guid[40] = {0};
::StringFromGUID2(guid_reason, guid, arraysize(guid));
const wchar_t* str = guid;
if (guid_reason == IID_IAuthenticate)
str = L"IAuthenticate";
else if (guid_reason == IID_IHttpSecurity)
str = L"IHttpSecurity";
else if (guid_reason == IID_IWindowForBindingUI)
str = L"IWindowForBindingUI";
DVLOG(1) << __FUNCTION__ << me() << "GetWindow: " << str;
#endif
// We should return a non-NULL HWND as parent. Otherwise no dialog is shown.
// TODO(iyengar): This hits when running the URL request tests.
DLOG_IF(WARNING, !::IsWindow(parent_window_))
<< "UrlmonUrlRequest::GetWindow - no window!";
*parent_window = parent_window_;
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::Authenticate(HWND* parent_window,
LPWSTR* user_name,
LPWSTR* password) {
if (!parent_window)
return E_INVALIDARG;
if (privileged_mode_)
return E_ACCESSDENIED;
DCHECK(::IsWindow(parent_window_));
*parent_window = parent_window_;
return S_OK;
}
STDMETHODIMP UrlmonUrlRequest::OnSecurityProblem(DWORD problem) {
// Urlmon notifies the client of authentication problems, certificate
// errors, etc by querying the object implementing the IBindStatusCallback
// interface for the IHttpSecurity interface. If this interface is not
// implemented then Urlmon checks for the problem codes defined below
// and performs actions as defined below:-
// It invokes the ReportProgress method of the protocol sink with
// these problem codes and eventually invokes the ReportResult method
// on the protocol sink which ends up in a call to the OnStopBinding
// method of the IBindStatusCallBack interface.
// MSHTML's implementation of the IBindStatusCallback interface does not
// implement the IHttpSecurity interface. However it handles the
// OnStopBinding call with a HRESULT of 0x800c0019 and navigates to
// an interstitial page which presents the user with a choice of whether
// to abort the navigation.
// In our OnStopBinding implementation we stop the navigation and inform
// Chrome about the result. Ideally Chrome should behave in a manner similar
// to IE, i.e. display the SSL error interstitial page and if the user
// decides to proceed anyway we would turn off SSL warnings for that
// particular navigation and allow IE to download the content.
// We would need to return the certificate information to Chrome for display
// purposes. Currently we only return a dummy certificate to Chrome.
// At this point we decided that it is a lot of work at this point and
// decided to go with the easier option of implementing the IHttpSecurity
// interface and replicating the checks performed by Urlmon. This
// causes Urlmon to display a dialog box on the same lines as IE6.
DVLOG(1) << __FUNCTION__ << me() << "Security problem : " << problem;
// On IE6 the default IBindStatusCallback interface does not implement the
// IHttpSecurity interface and thus causes IE to put up a certificate error
// dialog box. We need to emulate this behavior for sites with mismatched
// certificates to work.
if (GetIEVersion() == IE_6)
return S_FALSE;
HRESULT hr = E_ABORT;
switch (problem) {
case ERROR_INTERNET_SEC_CERT_REV_FAILED: {
hr = RPC_E_RETRY;
break;
}
case ERROR_INTERNET_SEC_CERT_DATE_INVALID:
case ERROR_INTERNET_SEC_CERT_CN_INVALID:
case ERROR_INTERNET_INVALID_CA: {
hr = S_FALSE;
break;
}
default: {
NOTREACHED() << "Unhandled security problem : " << problem;
break;
}
}
return hr;
}
HRESULT UrlmonUrlRequest::StartAsyncDownload() {
DVLOG(1) << __FUNCTION__ << me() << url();
HRESULT hr = E_FAIL;
DCHECK((moniker_ && bind_context_) || (!moniker_ && !bind_context_));
if (!moniker_.get()) {
std::wstring wide_url = UTF8ToWide(url());
hr = CreateURLMonikerEx(NULL, wide_url.c_str(), moniker_.Receive(),
URL_MK_UNIFORM);
if (FAILED(hr)) {
NOTREACHED() << "CreateURLMonikerEx failed. Error: " << hr;
return hr;
}
}
if (bind_context_.get() == NULL) {
hr = ::CreateAsyncBindCtxEx(NULL, 0, this, NULL,
bind_context_.Receive(), 0);
DCHECK(SUCCEEDED(hr)) << "CreateAsyncBindCtxEx failed. Error: " << hr;
} else {
// Use existing bind context.
hr = ::RegisterBindStatusCallback(bind_context_, this, NULL, 0);
DCHECK(SUCCEEDED(hr)) << "RegisterBindStatusCallback failed. Error: " << hr;
}
if (SUCCEEDED(hr)) {
base::win::ScopedComPtr<IStream> stream;
// BindToStorage may complete synchronously.
// We still get all the callbacks - OnStart/StopBinding, this may result
// in destruction of our object. It's fine but we access some members
// below for debug info. :)
base::win::ScopedComPtr<IHttpSecurity> self(this);
// Inform our moniker patch this binding should not be tortured.
base::win::ScopedComPtr<BindContextInfo> info;
BindContextInfo::FromBindContext(bind_context_, info.Receive());
DCHECK(info);
if (info)
info->set_chrome_request(true);
hr = moniker_->BindToStorage(bind_context_, NULL, __uuidof(IStream),
reinterpret_cast<void**>(stream.Receive()));
if (hr == S_OK)
DCHECK(binding_ != NULL || status_.get_state() == Status::DONE);
if (FAILED(hr)) {
// TODO(joshia): Look into. This currently fails for:
// http://user2:secret@localhost:1337/auth-basic?set-cookie-if-challenged
// when running the UrlRequest unit tests.
DLOG(ERROR) << __FUNCTION__ << me() <<
base::StringPrintf("IUrlMoniker::BindToStorage failed 0x%08X.", hr);
// In most cases we'll get a MK_E_SYNTAX error here but if we abort
// the navigation ourselves such as in the case of seeing something
// else than ALLOWALL in X-Frame-Options.
}
}
DLOG_IF(ERROR, FAILED(hr)) << me() <<
base::StringPrintf(L"StartAsyncDownload failed: 0x%08X", hr);
return hr;
}
void UrlmonUrlRequest::NotifyDelegateAndDie() {
DCHECK_EQ(thread_, base::PlatformThread::CurrentId());
DVLOG(1) << __FUNCTION__ << me();
PluginUrlRequestDelegate* delegate = delegate_;
delegate_ = NULL;
ReleaseBindings();
TerminateTransaction();
if (delegate && id() != -1) {
net::URLRequestStatus result = status_.get_result();
delegate->OnResponseEnd(id(), result);
} else {
DLOG(WARNING) << __FUNCTION__ << me() << "no delegate";
}
}
void UrlmonUrlRequest::TerminateTransaction() {
if (cleanup_transaction_ && bind_context_ && moniker_) {
// We return INET_E_TERMINATED_BIND from our OnDataAvailable implementation
// to ensure that the transaction stays around if Chrome decides to issue
// a download request when it finishes inspecting the headers received in
// OnResponse. However this causes the urlmon transaction object to leak.
// To workaround this we save away the IInternetProtocol interface which is
// implemented by the urlmon CTransaction object in our BindContextInfo
// instance which is maintained per bind context. Invoking Terminate
// on this with the special flags 0x2000000 cleanly releases the
// transaction.
static const int kUrlmonTerminateTransactionFlags = 0x2000000;
base::win::ScopedComPtr<BindContextInfo> info;
BindContextInfo::FromBindContext(bind_context_, info.Receive());
DCHECK(info);
if (info && info->protocol()) {
info->protocol()->Terminate(kUrlmonTerminateTransactionFlags);
}
}
bind_context_.Release();
}
void UrlmonUrlRequest::ReleaseBindings() {
binding_.Release();
// Do not release bind_context here!
// We may get DownloadToHost request and therefore we want the bind_context
// to be available.
if (bind_context_)
::RevokeBindStatusCallback(bind_context_, this);
}
net::Error UrlmonUrlRequest::HresultToNetError(HRESULT hr) {
const int kInvalidHostName = 0x8007007b;
// Useful reference:
// http://msdn.microsoft.com/en-us/library/ms775145(VS.85).aspx
net::Error ret = net::ERR_UNEXPECTED;
switch (hr) {
case S_OK:
ret = net::OK;
break;
case MK_E_SYNTAX:
ret = net::ERR_INVALID_URL;
break;
case INET_E_CANNOT_CONNECT:
ret = net::ERR_CONNECTION_FAILED;
break;
case INET_E_DOWNLOAD_FAILURE:
case INET_E_CONNECTION_TIMEOUT:
case E_ABORT:
ret = net::ERR_CONNECTION_ABORTED;
break;
case INET_E_DATA_NOT_AVAILABLE:
ret = net::ERR_EMPTY_RESPONSE;
break;
case INET_E_RESOURCE_NOT_FOUND:
// To behave more closely to the chrome network stack, we translate this
// error value as tunnel connection failed. This error value is tested
// in the ProxyTunnelRedirectTest and UnexpectedServerAuthTest tests.
ret = net::ERR_TUNNEL_CONNECTION_FAILED;
break;
// The following error codes can be returned while processing an invalid
// url. http://msdn.microsoft.com/en-us/library/bb250493(v=vs.85).aspx
case INET_E_INVALID_URL:
case INET_E_UNKNOWN_PROTOCOL:
case INET_E_REDIRECT_FAILED:
case INET_E_SECURITY_PROBLEM:
case kInvalidHostName:
case E_INVALIDARG:
case E_OUTOFMEMORY:
ret = net::ERR_INVALID_URL;
break;
case INET_E_INVALID_CERTIFICATE:
ret = net::ERR_CERT_INVALID;
break;
case E_ACCESSDENIED:
ret = net::ERR_ACCESS_DENIED;
break;
default:
DLOG(WARNING)
<< base::StringPrintf("TODO: translate HRESULT 0x%08X to net::Error",
hr);
break;
}
return ret;
}
PluginUrlRequestManager::ThreadSafeFlags
UrlmonUrlRequestManager::GetThreadSafeFlags() {
return PluginUrlRequestManager::NOT_THREADSAFE;
}
void UrlmonUrlRequestManager::SetInfoForUrl(const std::wstring& url,
IMoniker* moniker, LPBC bind_ctx) {
CComObject<UrlmonUrlRequest>* new_request = NULL;
CComObject<UrlmonUrlRequest>::CreateInstance(&new_request);
if (new_request) {
GURL start_url(url);
DCHECK(start_url.is_valid());
DCHECK(pending_request_ == NULL);
base::win::ScopedComPtr<BindContextInfo> info;
BindContextInfo::FromBindContext(bind_ctx, info.Receive());
DCHECK(info);
IStream* cache = info ? info->cache() : NULL;
pending_request_ = new_request;
pending_request_->InitPending(start_url, moniker, bind_ctx,
enable_frame_busting_, privileged_mode_,
notification_window_, cache);
// Start the request
bool is_started = pending_request_->Start();
DCHECK(is_started);
}
}
void UrlmonUrlRequestManager::StartRequest(int request_id,
const AutomationURLRequest& request_info) {
DVLOG(1) << __FUNCTION__ << " id: " << request_id;
if (stopping_) {
DLOG(WARNING) << __FUNCTION__ << " request not started (stopping)";
return;
}
DCHECK(request_map_.find(request_id) == request_map_.end());
#ifndef NDEBUG
if (background_worker_thread_enabled_) {
base::AutoLock lock(background_resource_map_lock_);
DCHECK(background_request_map_.find(request_id) ==
background_request_map_.end());
}
#endif // NDEBUG
DCHECK(GURL(request_info.url).is_valid());
// Non frame requests like sub resources, images, etc are handled on the
// background thread.
if (background_worker_thread_enabled_ &&
!ResourceType::IsFrame(
static_cast<ResourceType::Type>(request_info.resource_type))) {
DLOG(INFO) << "Downloading resource type "
<< request_info.resource_type
<< " on background thread";
background_thread_->message_loop()->PostTask(
FROM_HERE,
base::Bind(&UrlmonUrlRequestManager::StartRequestHelper,
base::Unretained(this), request_id, request_info,
&background_request_map_, &background_resource_map_lock_));
return;
}
StartRequestHelper(request_id, request_info, &request_map_, NULL);
}
void UrlmonUrlRequestManager::StartRequestHelper(
int request_id,
const AutomationURLRequest& request_info,
RequestMap* request_map,
base::Lock* request_map_lock) {
DCHECK(request_map);
scoped_refptr<UrlmonUrlRequest> new_request;
bool is_started = false;
if (pending_request_) {
if (pending_request_->url() != request_info.url) {
DLOG(INFO) << __FUNCTION__
<< "Received url request for url:"
<< request_info.url
<< ". Stopping pending url request for url:"
<< pending_request_->url();
pending_request_->Stop();
pending_request_ = NULL;
} else {
new_request.swap(pending_request_);
is_started = true;
DVLOG(1) << __FUNCTION__ << new_request->me()
<< " assigned id " << request_id;
}
}
if (!is_started) {
CComObject<UrlmonUrlRequest>* created_request = NULL;
CComObject<UrlmonUrlRequest>::CreateInstance(&created_request);
new_request = created_request;
}
// Format upload data if it's chunked.
if (request_info.upload_data && request_info.upload_data->is_chunked()) {
ScopedVector<net::UploadElement>* elements =
request_info.upload_data->elements_mutable();
for (size_t i = 0; i < elements->size(); ++i) {
net::UploadElement* element = (*elements)[i];
DCHECK(element->type() == net::UploadElement::TYPE_BYTES);
std::string chunk_length = base::StringPrintf(
"%X\r\n", static_cast<unsigned int>(element->bytes_length()));
std::vector<char> bytes;
bytes.insert(bytes.end(), chunk_length.data(),
chunk_length.data() + chunk_length.length());
const char* data = element->bytes();
bytes.insert(bytes.end(), data, data + element->bytes_length());
const char* crlf = "\r\n";
bytes.insert(bytes.end(), crlf, crlf + strlen(crlf));
if (i == elements->size() - 1) {
const char* end_of_data = "0\r\n\r\n";
bytes.insert(bytes.end(), end_of_data,
end_of_data + strlen(end_of_data));
}
element->SetToBytes(&bytes[0], static_cast<int>(bytes.size()));
}
}
new_request->Initialize(static_cast<PluginUrlRequestDelegate*>(this),
request_id,
request_info.url,
request_info.method,
request_info.referrer,
request_info.extra_request_headers,
request_info.upload_data,
static_cast<ResourceType::Type>(request_info.resource_type),
enable_frame_busting_,
request_info.load_flags);
new_request->set_parent_window(notification_window_);
new_request->set_privileged_mode(privileged_mode_);
if (request_map_lock)
request_map_lock->Acquire();
(*request_map)[request_id] = new_request;
if (request_map_lock)
request_map_lock->Release();
if (!is_started) {
// Freshly created, start now.
new_request->Start();
} else {
// Request is already underway, call OnResponse so that the
// other side can start reading.
DCHECK(!new_request->response_headers().empty());
new_request->OnResponse(
0, UTF8ToWide(new_request->response_headers()).c_str(), NULL, NULL);
}
}
void UrlmonUrlRequestManager::ReadRequest(int request_id, int bytes_to_read) {
DVLOG(1) << __FUNCTION__ << " id: " << request_id;
// if we fail to find the request in the normal map and the background
// request map, it may mean that the request could have failed with a
// network error.
scoped_refptr<UrlmonUrlRequest> request = LookupRequest(request_id,
&request_map_);
if (request) {
request->Read(bytes_to_read);
} else if (background_worker_thread_enabled_) {
base::AutoLock lock(background_resource_map_lock_);
request = LookupRequest(request_id, &background_request_map_);
if (request) {
background_thread_->message_loop()->PostTask(
FROM_HERE, base::Bind(base::IgnoreResult(&UrlmonUrlRequest::Read),
request.get(), bytes_to_read));
}
}
if (!request)
DLOG(ERROR) << __FUNCTION__ << " no request found for " << request_id;
}
void UrlmonUrlRequestManager::DownloadRequestInHost(int request_id) {
DVLOG(1) << __FUNCTION__ << " " << request_id;
if (!IsWindow(notification_window_)) {
NOTREACHED() << "Cannot handle download if we don't have anyone to hand it "
"to.";
return;
}
scoped_refptr<UrlmonUrlRequest> request(LookupRequest(request_id,
&request_map_));
if (request) {
DownloadRequestInHostHelper(request);
} else if (background_worker_thread_enabled_) {
base::AutoLock lock(background_resource_map_lock_);
request = LookupRequest(request_id, &background_request_map_);
if (request) {
background_thread_->message_loop()->PostTask(
FROM_HERE,
base::Bind(&UrlmonUrlRequestManager::DownloadRequestInHostHelper,
base::Unretained(this), request.get()));
}
}
if (!request)
DLOG(ERROR) << __FUNCTION__ << " no request found for " << request_id;
}
void UrlmonUrlRequestManager::DownloadRequestInHostHelper(
UrlmonUrlRequest* request) {
DCHECK(request);
UrlmonUrlRequest::TerminateBindCallback callback =
base::Bind(&UrlmonUrlRequestManager::BindTerminated,
base::Unretained(this));
request->TerminateBind(callback);
}
void UrlmonUrlRequestManager::BindTerminated(IMoniker* moniker,
IBindCtx* bind_ctx,
IStream* post_data,
const char* request_headers) {
DownloadInHostParams* download_params = new DownloadInHostParams;
download_params->bind_ctx = bind_ctx;
download_params->moniker = moniker;
download_params->post_data = post_data;
if (request_headers) {
download_params->request_headers = request_headers;
}
::PostMessage(notification_window_, WM_DOWNLOAD_IN_HOST,
reinterpret_cast<WPARAM>(download_params), 0);
}
void UrlmonUrlRequestManager::EndRequest(int request_id) {
DVLOG(1) << __FUNCTION__ << " id: " << request_id;
scoped_refptr<UrlmonUrlRequest> request = LookupRequest(request_id,
&request_map_);
if (request) {
request_map_.erase(request_id);
request->Stop();
} else if (background_worker_thread_enabled_) {
base::AutoLock lock(background_resource_map_lock_);
request = LookupRequest(request_id, &background_request_map_);
if (request) {
background_request_map_.erase(request_id);
background_thread_->message_loop()->PostTask(
FROM_HERE, base::Bind(&UrlmonUrlRequest::Stop, request.get()));
}
}
if (!request)
DLOG(ERROR) << __FUNCTION__ << " no request found for " << request_id;
}
void UrlmonUrlRequestManager::StopAll() {
DVLOG(1) << __FUNCTION__;
if (stopping_)
return;
stopping_ = true;
DVLOG(1) << __FUNCTION__ << " stopping " << request_map_.size()
<< " requests";
StopAllRequestsHelper(&request_map_, NULL);
if (background_worker_thread_enabled_) {
DCHECK(background_thread_.get());
background_thread_->message_loop()->PostTask(
FROM_HERE, base::Bind(&UrlmonUrlRequestManager::StopAllRequestsHelper,
base::Unretained(this), &background_request_map_,
&background_resource_map_lock_));
background_thread_->Stop();
background_thread_.reset();
}
}
void UrlmonUrlRequestManager::StopAllRequestsHelper(
RequestMap* request_map,
base::Lock* request_map_lock) {
DCHECK(request_map);
DVLOG(1) << __FUNCTION__ << " stopping " << request_map->size()
<< " requests";
if (request_map_lock)
request_map_lock->Acquire();
for (RequestMap::iterator it = request_map->begin();
it != request_map->end(); ++it) {
DCHECK(it->second != NULL);
it->second->Stop();
}
request_map->clear();
if (request_map_lock)
request_map_lock->Release();
}
void UrlmonUrlRequestManager::OnResponseStarted(
int request_id, const char* mime_type, const char* headers, int size,
base::Time last_modified, const std::string& redirect_url,
int redirect_status, const net::HostPortPair& socket_address,
uint64 upload_size) {
DCHECK_NE(request_id, -1);
DVLOG(1) << __FUNCTION__;
#ifndef NDEBUG
scoped_refptr<UrlmonUrlRequest> request = LookupRequest(request_id,
&request_map_);
if (request == NULL && background_worker_thread_enabled_) {
base::AutoLock lock(background_resource_map_lock_);
request = LookupRequest(request_id, &background_request_map_);
}
DCHECK(request != NULL);
#endif // NDEBUG
delegate_->OnResponseStarted(
request_id, mime_type, headers, size, last_modified, redirect_url,
redirect_status, socket_address, upload_size);
}
void UrlmonUrlRequestManager::OnReadComplete(int request_id,
const std::string& data) {
DCHECK_NE(request_id, -1);
DVLOG(1) << __FUNCTION__ << " id: " << request_id;
#ifndef NDEBUG
scoped_refptr<UrlmonUrlRequest> request = LookupRequest(request_id,
&request_map_);
if (request == NULL && background_worker_thread_enabled_) {
base::AutoLock lock(background_resource_map_lock_);
request = LookupRequest(request_id, &background_request_map_);
}
DCHECK(request != NULL);
#endif // NDEBUG
delegate_->OnReadComplete(request_id, data);
DVLOG(1) << __FUNCTION__ << " done id: " << request_id;
}
void UrlmonUrlRequestManager::OnResponseEnd(
int request_id,
const net::URLRequestStatus& status) {
DCHECK_NE(request_id, -1);
DVLOG(1) << __FUNCTION__;
DCHECK(status.status() != net::URLRequestStatus::CANCELED);
RequestMap::size_type erased_count = request_map_.erase(request_id);
if (erased_count != 1u && background_worker_thread_enabled_) {
base::AutoLock lock(background_resource_map_lock_);
erased_count = background_request_map_.erase(request_id);
if (erased_count != 1u) {
DLOG(WARNING) << __FUNCTION__
<< " Failed to find request id:"
<< request_id;
}
}
delegate_->OnResponseEnd(request_id, status);
}
scoped_refptr<UrlmonUrlRequest> UrlmonUrlRequestManager::LookupRequest(
int request_id, RequestMap* request_map) {
RequestMap::iterator it = request_map->find(request_id);
if (request_map->end() != it)
return it->second;
return NULL;
}
UrlmonUrlRequestManager::UrlmonUrlRequestManager()
: stopping_(false), notification_window_(NULL),
privileged_mode_(false),
container_(NULL),
background_worker_thread_enabled_(true) {
background_thread_.reset(new base::Thread("cf_iexplore_background_thread"));
background_thread_->init_com_with_mta(false);
background_worker_thread_enabled_ =
GetConfigBool(true, kUseBackgroundThreadForSubResources);
if (background_worker_thread_enabled_) {
base::Thread::Options options;
options.message_loop_type = base::MessageLoop::TYPE_UI;
background_thread_->StartWithOptions(options);
}
}
UrlmonUrlRequestManager::~UrlmonUrlRequestManager() {
StopAll();
}
void UrlmonUrlRequestManager::AddPrivacyDataForUrl(
const std::string& url, const std::string& policy_ref,
int32 flags) {
DCHECK(!url.empty());
bool fire_privacy_event = false;
if (privacy_info_.privacy_records.empty())
flags |= PRIVACY_URLISTOPLEVEL;
if (!privacy_info_.privacy_impacted) {
if (flags & (COOKIEACTION_ACCEPT | COOKIEACTION_REJECT |
COOKIEACTION_DOWNGRADE)) {
privacy_info_.privacy_impacted = true;
fire_privacy_event = true;
}
}
PrivacyInfo::PrivacyEntry& privacy_entry =
privacy_info_.privacy_records[UTF8ToWide(url)];
privacy_entry.flags |= flags;
privacy_entry.policy_ref = UTF8ToWide(policy_ref);
if (fire_privacy_event && IsWindow(notification_window_)) {
PostMessage(notification_window_, WM_FIRE_PRIVACY_CHANGE_NOTIFICATION, 1,
0);
}
}