diff options
29 files changed, 3507 insertions, 2 deletions
diff --git a/ios/net/request_tracker.h b/ios/net/request_tracker.h index e47b732..f21cb88 100644 --- a/ios/net/request_tracker.h +++ b/ios/net/request_tracker.h @@ -44,6 +44,8 @@ class RequestTracker { class RequestTrackerFactory { public: + virtual ~RequestTrackerFactory(); + // Returns false if |request| is associated to an invalid tracker and should // be cancelled. In this case |tracker| is set to nullptr. // Returns true if |request| is associated with a valid tracker or if the diff --git a/ios/net/request_tracker.mm b/ios/net/request_tracker.mm index c984f36..6eb21c3 100644 --- a/ios/net/request_tracker.mm +++ b/ios/net/request_tracker.mm @@ -66,6 +66,9 @@ GlobalNetworkClientFactories* } // namespace +RequestTracker::RequestTrackerFactory::~RequestTrackerFactory() { +} + // static void RequestTracker::SetRequestTrackerFactory(RequestTrackerFactory* factory) { g_request_tracker_factory = factory; diff --git a/ios/web/browser_state.cc b/ios/web/browser_state.cc index 5a8c032..c7ce99f 100644 --- a/ios/web/browser_state.cc +++ b/ios/web/browser_state.cc @@ -4,11 +4,44 @@ #include "ios/web/public/browser_state.h" +#include "base/memory/ref_counted.h" +#include "ios/web/public/certificate_policy_cache.h" +#include "ios/web/public/web_thread.h" + namespace web { namespace { // Private key used for safe conversion of base::SupportsUserData to // web::BrowserState in web::BrowserState::FromSupportsUserData. const char kBrowserStateIdentifierKey[] = "BrowserStateIdentifierKey"; +// Data key names. +const char kCertificatePolicyCacheKeyName[] = "cert_policy_cache"; + +// Wraps a CertificatePolicyCache as a SupportsUserData::Data; this is necessary +// since reference counted objects can't be user data. +struct CertificatePolicyCacheHandle : public base::SupportsUserData::Data { + explicit CertificatePolicyCacheHandle(CertificatePolicyCache* cache) + : policy_cache(cache) {} + + scoped_refptr<CertificatePolicyCache> policy_cache; +}; +} + +// static +scoped_refptr<CertificatePolicyCache> BrowserState::GetCertificatePolicyCache( + BrowserState* browser_state) { + DCHECK_CURRENTLY_ON_WEB_THREAD(WebThread::UI); + if (!browser_state->GetUserData(kCertificatePolicyCacheKeyName)) { + CertificatePolicyCacheHandle* cert_cache_service_handle = + new CertificatePolicyCacheHandle(new CertificatePolicyCache()); + + browser_state->SetUserData(kCertificatePolicyCacheKeyName, + cert_cache_service_handle); + } + + CertificatePolicyCacheHandle* handle = + static_cast<CertificatePolicyCacheHandle*>( + browser_state->GetUserData(kCertificatePolicyCacheKeyName)); + return handle->policy_cache; } BrowserState::BrowserState() { diff --git a/ios/web/ios_web.gyp b/ios/web/ios_web.gyp index 478fd7f..ab73e90 100644 --- a/ios/web/ios_web.gyp +++ b/ios/web/ios_web.gyp @@ -19,9 +19,12 @@ 'user_agent', '../../base/base.gyp:base', '../../content/content.gyp:content_browser', + '../../ios/net/ios_net.gyp:ios_net', + '../../ios/third_party/blink/blink_html_tokenizer.gyp:blink_html_tokenizer', '../../net/net.gyp:net', '../../ui/base/ui_base.gyp:ui_base', '../../ui/gfx/gfx.gyp:gfx', + '../../url/url.gyp:url_lib', ], 'sources': [ 'browser_state.cc', @@ -33,13 +36,26 @@ 'navigation/time_smoother.cc', 'navigation/time_smoother.h', 'net/cert_policy.cc', + 'net/cert_store_impl.cc', + 'net/cert_store_impl.h', 'net/certificate_policy_cache.cc', + 'net/cookie_notification_bridge.h', + 'net/cookie_notification_bridge.mm', + 'net/crw_request_tracker_delegate.h', 'net/request_group_util.h', 'net/request_group_util.mm', + 'net/request_tracker_data_memoizing_store.h', + 'net/request_tracker_factory_impl.h', + 'net/request_tracker_factory_impl.mm', + 'net/request_tracker_impl.h', + 'net/request_tracker_impl.mm', + 'net/web_http_protocol_handler_delegate.h', + 'net/web_http_protocol_handler_delegate.mm', 'public/block_types.h', 'public/browser_state.h', 'public/browser_url_rewriter.h', 'public/cert_policy.h', + 'public/cert_store.h', 'public/certificate_policy_cache.h', 'public/favicon_status.cc', 'public/favicon_status.h', @@ -84,6 +100,8 @@ 'public/web_thread.h', 'public/web_view_type.h', 'string_util.cc', + 'ui_web_view_util.h', + 'ui_web_view_util.mm', 'url_scheme_util.mm', 'url_util.cc', 'user_metrics.cc', @@ -102,6 +120,8 @@ 'web_state/ui/crw_debug_web_view.h', 'web_state/ui/crw_debug_web_view.mm', 'web_state/ui/crw_simple_web_view_controller.h', + 'web_state/ui/crw_static_file_web_view.h', + 'web_state/ui/crw_static_file_web_view.mm', 'web_state/web_state_observer.cc', 'web_state/web_state_observer_bridge.mm', 'web_state/wk_web_view_ssl_error_util.h', @@ -233,8 +253,12 @@ 'target_name': 'test_support_ios_web', 'type': 'static_library', 'dependencies': [ - '../../content/content_shell_and_tests.gyp:test_support_content', 'ios_web', + '../../content/content_shell_and_tests.gyp:test_support_content', + '../../ios/testing/ios_testing.gyp:ocmock_support', + '../../testing/gmock.gyp:gmock', + '../../testing/gtest.gyp:gtest', + '../../third_party/ocmock/ocmock.gyp:ocmock', ], 'include_dirs': [ '../..', @@ -246,6 +270,8 @@ 'public/test/js_test_util.mm', 'public/test/test_browser_state.cc', 'public/test/test_browser_state.h', + 'public/test/test_web_client.h', + 'public/test/test_web_client.mm', 'public/test/test_web_state.cc', 'public/test/test_web_state.h', 'public/test/test_web_thread.h', diff --git a/ios/web/ios_web_unittests.gyp b/ios/web/ios_web_unittests.gyp index d0a04bd..f2e0816 100644 --- a/ios/web/ios_web_unittests.gyp +++ b/ios/web/ios_web_unittests.gyp @@ -4,7 +4,7 @@ { 'variables': { 'chromium_code': 1, - }, + }, 'targets': [ { 'target_name': 'ios_web_unittests', @@ -15,6 +15,8 @@ '../../base/base.gyp:test_support_base', '../../testing/gmock.gyp:gmock', '../../testing/gtest.gyp:gtest', + '../../third_party/ocmock/ocmock.gyp:ocmock', + '../testing/ios_testing.gyp:ocmock_support', 'ios_web.gyp:ios_web', 'ios_web.gyp:test_support_ios_web', ], @@ -26,11 +28,15 @@ 'navigation/nscoder_util_unittest.mm', 'net/cert_policy_unittest.cc', 'net/request_group_util_unittest.mm', + 'net/request_tracker_impl_unittest.mm', + 'net/web_http_protocol_handler_delegate_unittest.mm', 'public/referrer_util_unittest.cc', 'string_util_unittest.cc', + 'ui_web_view_util_unittest.mm', 'url_scheme_util_unittest.mm', 'url_util_unittest.cc', 'weak_nsobject_counter_unittest.mm', + 'web_state/ui/crw_static_file_web_view_unittest.mm', ], }, ], diff --git a/ios/web/net/cert_store_impl.cc b/ios/web/net/cert_store_impl.cc new file mode 100644 index 0000000..9cccbab --- /dev/null +++ b/ios/web/net/cert_store_impl.cc @@ -0,0 +1,38 @@ +// Copyright 2015 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 "ios/web/net/cert_store_impl.h" + +namespace web { + +// static +CertStore* CertStore::GetInstance() { + return CertStoreImpl::GetInstance(); +} + +// static +CertStoreImpl* CertStoreImpl::GetInstance() { + return Singleton<CertStoreImpl>::get(); +} + +CertStoreImpl::CertStoreImpl() { +} + +CertStoreImpl::~CertStoreImpl() { +} + +int CertStoreImpl::StoreCert(net::X509Certificate* cert, int group_id) { + return store_.Store(cert, group_id); +} + +bool CertStoreImpl::RetrieveCert(int cert_id, + scoped_refptr<net::X509Certificate>* cert) { + return store_.Retrieve(cert_id, cert); +} + +void CertStoreImpl::RemoveCertsForGroup(int group_id) { + store_.RemoveForRequestTracker(group_id); +} + +} // namespace web diff --git a/ios/web/net/cert_store_impl.h b/ios/web/net/cert_store_impl.h new file mode 100644 index 0000000..8e2154c --- /dev/null +++ b/ios/web/net/cert_store_impl.h @@ -0,0 +1,40 @@ +// Copyright 2015 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. + +#ifndef IOS_WEB_NET_CERT_STORE_IMPL_H_ +#define IOS_WEB_NET_CERT_STORE_IMPL_H_ + +#include "base/memory/singleton.h" +#include "ios/web/net/request_tracker_data_memoizing_store.h" +#include "ios/web/public/cert_store.h" +#include "net/cert/x509_certificate.h" + +namespace web { + +class CertStoreImpl : public CertStore { + public: + // Returns the singleton instance of the CertStore. + static CertStoreImpl* GetInstance(); + + // CertStore implementation: + int StoreCert(net::X509Certificate* cert, int group_id) override; + bool RetrieveCert(int cert_id, + scoped_refptr<net::X509Certificate>* cert) override; + void RemoveCertsForGroup(int group_id) override; + + protected: + CertStoreImpl(); + ~CertStoreImpl() override; + + private: + friend struct DefaultSingletonTraits<CertStoreImpl>; + + RequestTrackerDataMemoizingStore<net::X509Certificate> store_; + + DISALLOW_COPY_AND_ASSIGN(CertStoreImpl); +}; + +} // namespace web + +#endif // IOS_WEB_NET_CERT_STORE_IMPL_H_ diff --git a/ios/web/net/cookie_notification_bridge.h b/ios/web/net/cookie_notification_bridge.h new file mode 100644 index 0000000..36fc706 --- /dev/null +++ b/ios/web/net/cookie_notification_bridge.h @@ -0,0 +1,31 @@ +// Copyright 2014 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. + +#ifndef IOS_WEB_NET_COOKIE_NOTIFICATION_BRIDGE_H_ +#define IOS_WEB_NET_COOKIE_NOTIFICATION_BRIDGE_H_ + +#include "base/mac/scoped_nsobject.h" +#include "base/threading/thread_checker.h" + +namespace web { + +// CookieNotificationBridge listens to +// NSHTTPCookieManagerCookiesChangedNotification on the main thread and re-sends +// it to the cookie store on the IO thread. +class CookieNotificationBridge { + public: + CookieNotificationBridge(); + ~CookieNotificationBridge(); + + private: + void OnNotificationReceived(NSNotification* notification); + base::scoped_nsprotocol<id> observer_; + base::ThreadChecker thread_checker_; + + DISALLOW_COPY_AND_ASSIGN(CookieNotificationBridge); +}; + +} // namespace web + +#endif // IOS_WEB_NET_COOKIE_NOTIFICATION_BRIDGE_H_ diff --git a/ios/web/net/cookie_notification_bridge.mm b/ios/web/net/cookie_notification_bridge.mm new file mode 100644 index 0000000..8ebb9a7 --- /dev/null +++ b/ios/web/net/cookie_notification_bridge.mm @@ -0,0 +1,40 @@ +// Copyright 2014 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 "ios/web/net/cookie_notification_bridge.h" + +#import <Foundation/Foundation.h> + +#include "base/bind.h" +#include "base/location.h" +#include "ios/net/cookies/cookie_store_ios.h" +#include "ios/web/public/web_thread.h" + +namespace web { + +CookieNotificationBridge::CookieNotificationBridge() { + observer_.reset([[NSNotificationCenter defaultCenter] + addObserverForName:NSHTTPCookieManagerCookiesChangedNotification + object:[NSHTTPCookieStorage sharedHTTPCookieStorage] + queue:nil + usingBlock:^(NSNotification* notification) { + OnNotificationReceived(notification); + }]); +} + +CookieNotificationBridge::~CookieNotificationBridge() { + [[NSNotificationCenter defaultCenter] removeObserver:observer_]; +} + +void CookieNotificationBridge::OnNotificationReceived( + NSNotification* notification) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK([[notification name] + isEqualToString:NSHTTPCookieManagerCookiesChangedNotification]); + web::WebThread::PostTask( + web::WebThread::IO, FROM_HERE, + base::Bind(&net::CookieStoreIOS::NotifySystemCookiesChanged)); +} + +} // namespace web diff --git a/ios/web/net/crw_request_tracker_delegate.h b/ios/web/net/crw_request_tracker_delegate.h new file mode 100644 index 0000000..2e8d827 --- /dev/null +++ b/ios/web/net/crw_request_tracker_delegate.h @@ -0,0 +1,71 @@ +// Copyright 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. + +#ifndef IOS_WEB_NET_CRW_REQUEST_TRACKER_DELEGATE_H_ +#define IOS_WEB_NET_CRW_REQUEST_TRACKER_DELEGATE_H_ + +#include <vector> + +#include "net/cert/cert_status_flags.h" + +class GURL; + +namespace net { +class HttpResponseHeaders; +class SSLInfo; +class X509Certificate; +} + +namespace web { +struct SSLStatus; +} + +// All the methods in this protocol must be sent on the main thread. +@protocol CRWRequestTrackerDelegate + +// Returns |YES| of all the requests are static file requests and returns |NO| +// if all the requests are network requests. Note it is not allowed for a +// |CRWRequestTrackerDelegate| to send both static file requests and network +// requests. +- (BOOL)isForStaticFileRequests; + +// The tracker calls this method every time there is a change in the SSL status +// of a page. The info is whatever object was passed to TrimToURL(). +- (void)updatedSSLStatus:(const web::SSLStatus&)sslStatus + forPageUrl:(const GURL&)url + userInfo:(id)userInfo; + +// The tracker calls this method when it receives response headers. +- (void)handleResponseHeaders:(net::HttpResponseHeaders*)headers + requestUrl:(const GURL&)requestUrl; + +// This method is called when a network request has an issue with the SSL +// connection to present it to the user. The user will decide if the request +// should continue or not and the callback should be invoked to let the backend +// know. +// If the callback is not called the request will be cancelled on the next call +// to TrimToURL(). +// The callback is safe to call until the requestTracker it originated from +// is deleted. +typedef void (^SSLErrorCallback)(BOOL); +- (void)presentSSLError:(const net::SSLInfo&)info + forSSLStatus:(const web::SSLStatus&)status + onUrl:(const GURL&)url + recoverable:(BOOL)recoverable + callback:(SSLErrorCallback)shouldContinue; + +// Update the progress. +- (void)updatedProgress:(float)progress; + +// This method is called when a certificate with an error is in use. +- (void)certificateUsed:(net::X509Certificate*)certificate + forHost:(const std::string&)host + status:(net::CertStatus)status; + +// Called when all the active allowed certificates need to be cleared. This +// happens during the TrimToURL(), which corresponds to a navigation. +- (void)clearCertificates; +@end + +#endif // IOS_WEB_NET_CRW_REQUEST_TRACKER_DELEGATE_H_ diff --git a/ios/web/net/request_tracker_data_memoizing_store.h b/ios/web/net/request_tracker_data_memoizing_store.h new file mode 100644 index 0000000..7ec3e32 --- /dev/null +++ b/ios/web/net/request_tracker_data_memoizing_store.h @@ -0,0 +1,182 @@ +// Copyright 2015 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. + +#ifndef IOS_WEB_NET_REQUEST_TRACKER_DATA_MEMOIZING_STORE_H_ +#define IOS_WEB_NET_REQUEST_TRACKER_DATA_MEMOIZING_STORE_H_ + +#include <map> + +#include "base/bind.h" +#include "base/synchronization/lock.h" +#include "ios/web/public/web_thread.h" + +namespace web { + +// RequestTrackerDataMemoizingStore is a thread-safe container that retains +// reference counted objects that are associated with one or more request +// trackers. Objects are identified by an int and only a single reference to a +// given object is retained. +template <typename T> +class RequestTrackerDataMemoizingStore { + public: + RequestTrackerDataMemoizingStore() : next_item_id_(1) {} + + ~RequestTrackerDataMemoizingStore() { + DCHECK_EQ(0U, id_to_item_.size()) << "Failed to outlive request tracker"; + } + + // Store adds |item| to this collection, associates it with the given request + // tracker id and returns an opaque identifier for it. If |item| is already + // known, the same identifier will be returned. + int Store(T* item, int request_tracker_id) { + DCHECK(item); + base::AutoLock auto_lock(lock_); + + int item_id = 0; + + // Is this item alread known? + typename ReverseItemMap::iterator item_iter = item_to_id_.find(item); + if (item_iter == item_to_id_.end()) { + item_id = next_item_id_++; + // Use 0 as an invalid item_id value. In the unlikely event that + // next_item_id_ wraps around, reset it to 1. + if (next_item_id_ == 0) + next_item_id_ = 1; + id_to_item_[item_id] = item; + item_to_id_[item] = item_id; + } else { + item_id = item_iter->second; + } + + // Update the bidirection request_tracker_id and item_id mappings. + UpdateMap(&request_tracker_id_to_item_id_, request_tracker_id, item_id); + UpdateMap(&item_id_to_request_tracker_id_, item_id, request_tracker_id); + + DCHECK(item_id); + return item_id; + } + + // Retrieves a previously Stored() item, identified by |item_id|. + // If |item_id| is recognized, |item| will be updated and Retrieve() will + // return true. + bool Retrieve(int item_id, scoped_refptr<T>* item) { + base::AutoLock auto_lock(lock_); + + typename ItemMap::iterator iter = id_to_item_.find(item_id); + if (iter == id_to_item_.end()) + return false; + if (item) + *item = iter->second; + return true; + } + + // Removes any items associtated only with |request_tracker_id|. + void RemoveForRequestTracker(int request_tracker_id) { + RemoveRequestTrackerItems(request_tracker_id); + } + + private: + typedef std::multimap<int, int> IDMap; + typedef std::map<int, scoped_refptr<T>> ItemMap; + typedef std::map<T*, int, typename T::LessThan> ReverseItemMap; + + template <typename M> + struct MatchSecond { + explicit MatchSecond(const M& t) : value(t) {} + + template <typename Pair> + bool operator()(const Pair& p) const { + return (value == p.second); + } + + M value; + }; + + // Updates |map| to contain a key_id->value_id pair, ensuring that a duplicate + // pair is not added if it was already present. + static void UpdateMap(IDMap* map, int key_id, int value_id) { + auto matching_key_range = map->equal_range(key_id); + if (std::find_if(matching_key_range.first, matching_key_range.second, + MatchSecond<int>(value_id)) == matching_key_range.second) { + map->insert(std::make_pair(key_id, value_id)); + } + } + + // Remove the item specified by |item_id| from id_to_item_ and item_to_id_. + // NOTE: the caller (RemoveRequestTrackerItems) must hold lock_. + void RemoveInternal(int item_id) { + typename ItemMap::iterator item_iter = id_to_item_.find(item_id); + DCHECK(item_iter != id_to_item_.end()); + + typename ReverseItemMap::iterator id_iter = + item_to_id_.find(item_iter->second.get()); + DCHECK(id_iter != item_to_id_.end()); + item_to_id_.erase(id_iter); + + id_to_item_.erase(item_iter); + } + + // Removes all the items associated with the specified request tracker from + // the store. + void RemoveRequestTrackerItems(int request_tracker_id) { + base::AutoLock auto_lock(lock_); + + // Iterate through all the item ids for that request tracker. + auto request_tracker_ids = + request_tracker_id_to_item_id_.equal_range(request_tracker_id); + for (IDMap::iterator ids_iter = request_tracker_ids.first; + ids_iter != request_tracker_ids.second; ++ids_iter) { + int item_id = ids_iter->second; + // Find all the request trackers referring to this item id in + // item_id_to_request_tracker_id_, then locate the request tracker being + // removed within that range. + auto item_ids = item_id_to_request_tracker_id_.equal_range(item_id); + IDMap::iterator proc_iter = + std::find_if(item_ids.first, item_ids.second, + MatchSecond<int>(request_tracker_id)); + DCHECK(proc_iter != item_ids.second); + + // Before removing, determine if no other request trackers refer to the + // current item id. If |proc_iter| (the current request tracker) is the + // lower bound of request trackers containing the current item id and if + // |next_proc_iter| is the upper bound (the first request tracker that + // does not), then only one request tracker, the one being removed, refers + // to the item id. + IDMap::iterator next_proc_iter = proc_iter; + ++next_proc_iter; + bool last_request_tracker_for_item_id = + (proc_iter == item_ids.first && next_proc_iter == item_ids.second); + item_id_to_request_tracker_id_.erase(proc_iter); + + if (last_request_tracker_for_item_id) { + // The current item id is not referenced by any other request trackers, + // so remove it from id_to_item_ and item_to_id_. + RemoveInternal(item_id); + } + } + if (request_tracker_ids.first != request_tracker_ids.second) + request_tracker_id_to_item_id_.erase(request_tracker_ids.first, + request_tracker_ids.second); + } + + // Bidirectional mapping between items and the request trackers they are + // associated with. + IDMap request_tracker_id_to_item_id_; + IDMap item_id_to_request_tracker_id_; + // Bidirectional mappings between items and the item-level identifiers that + // have been assigned to them. + ItemMap id_to_item_; + ReverseItemMap item_to_id_; + + // The ID to assign to the next item stored. + int next_item_id_; + + // This lock protects: request_tracker_id_to_item_id_, + // item_id_to_request_tracker_id_, id_to_item_, and item_to_id_. + base::Lock lock_; +}; + +} // namespace web + +#endif // IOS_WEB_NET_REQUEST_TRACKER_DATA_MEMOIZING_STORE_H_ diff --git a/ios/web/net/request_tracker_factory_impl.h b/ios/web/net/request_tracker_factory_impl.h new file mode 100644 index 0000000..2d0bfc4 --- /dev/null +++ b/ios/web/net/request_tracker_factory_impl.h @@ -0,0 +1,31 @@ +// Copyright 2014 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. + +#ifndef IOS_WEB_NET_REQUEST_TRACKER_FACTORY_IMPL_H_ +#define IOS_WEB_NET_REQUEST_TRACKER_FACTORY_IMPL_H_ + +#include <string> + +#include "base/mac/scoped_nsobject.h" +#include "ios/net/request_tracker.h" + +namespace web { + +class RequestTrackerFactoryImpl + : public net::RequestTracker::RequestTrackerFactory { + public: + explicit RequestTrackerFactoryImpl(const std::string& application_scheme); + ~RequestTrackerFactoryImpl() override; + + private: + // RequestTracker::RequestTrackerFactory implementation + bool GetRequestTracker(NSURLRequest* request, + base::WeakPtr<net::RequestTracker>* tracker) override; + + base::scoped_nsobject<NSString> application_scheme_; +}; + +} // namespace web + +#endif // IOS_WEB_NET_REQUEST_TRACKER_FACTORY_IMPL_H_ diff --git a/ios/web/net/request_tracker_factory_impl.mm b/ios/web/net/request_tracker_factory_impl.mm new file mode 100644 index 0000000..8973d3c --- /dev/null +++ b/ios/web/net/request_tracker_factory_impl.mm @@ -0,0 +1,49 @@ +// Copyright 2014 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 "ios/web/net/request_tracker_factory_impl.h" + +#include "base/logging.h" +#include "base/memory/weak_ptr.h" +#include "base/strings/sys_string_conversions.h" +#include "ios/web/net/request_group_util.h" +#include "ios/web/net/request_tracker_impl.h" + +namespace web { + +RequestTrackerFactoryImpl::RequestTrackerFactoryImpl( + const std::string& application_scheme) { + if (!application_scheme.empty()) { + application_scheme_.reset( + [base::SysUTF8ToNSString(application_scheme) copy]); + DCHECK(application_scheme_); + } +} + +RequestTrackerFactoryImpl::~RequestTrackerFactoryImpl() { +} + +bool RequestTrackerFactoryImpl::GetRequestTracker( + NSURLRequest* request, + base::WeakPtr<net::RequestTracker>* tracker) { + DCHECK(tracker); + DCHECK(!tracker->get()); + NSString* request_group_id = + web::ExtractRequestGroupIDFromRequest(request, application_scheme_); + if (!request_group_id) { + // There was no request_group_id, so the request was from something like a + // data: or file: URL. + return true; + } + RequestTrackerImpl* tracker_impl = + RequestTrackerImpl::GetTrackerForRequestGroupID(request_group_id); + if (tracker_impl) + *tracker = tracker_impl->GetWeakPtr(); + // If there is a request group ID, but no associated tracker, return false. + // This usually happens when the tab has been closed, but can maybe also + // happen in other cases (see http://crbug.com/228397). + return tracker->get() != nullptr; +} + +} // namespace web diff --git a/ios/web/net/request_tracker_impl.h b/ios/web/net/request_tracker_impl.h new file mode 100644 index 0000000..45aaa03 --- /dev/null +++ b/ios/web/net/request_tracker_impl.h @@ -0,0 +1,399 @@ +// Copyright 2014 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. + +#ifndef IOS_WEB_NET_REQUEST_TRACKER_IMPL_H_ +#define IOS_WEB_NET_REQUEST_TRACKER_IMPL_H_ + +#import <Foundation/Foundation.h> +#include <map> +#include <set> + +#include "base/callback_forward.h" +#include "base/mac/scoped_nsobject.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_vector.h" +#include "base/memory/weak_ptr.h" +#import "ios/net/request_tracker.h" +#import "ios/web/net/crw_request_tracker_delegate.h" +#include "ios/web/public/web_thread.h" +#include "net/url_request/url_request_context_getter.h" +#include "url/gurl.h" + +@class SSLCarrier; +@class CRWSSLCarrier; +class SSLErrorInfo; +struct TrackerCounts; + +namespace content { +struct SSLStatus; +} + +namespace net { +class HttpResponseHeaders; +class URLRequest; +class URLRequestContext; +class SSLInfo; +class X509Certificate; +} + +namespace web { + +class BrowserState; +class CertificatePolicyCache; + +// Structure to capture the current state of a page. +struct PageCounts { + public: + PageCounts() : finished(0), + finished_bytes(0), + unfinished(0), + unfinished_no_estimate(0), + unfinished_no_estimate_bytes_done(0), + unfinished_estimated_bytes_left(0), + unfinished_estimate_bytes_done(0), + largest_byte_size_known(0) { + }; + + // Count of finished requests. + uint64_t finished; + // Total bytes count dowloaded for all finished requests. + uint64_t finished_bytes; + // Count of unfinished requests. + uint64_t unfinished; + // Count of unfinished requests with unknown size. + uint64_t unfinished_no_estimate; + // Total bytes count dowloaded for unfinished requests of unknown size. + uint64_t unfinished_no_estimate_bytes_done; + // Count of unfinished requests with an estimated size. + uint64_t unfinished_estimated_bytes_left; + // Total bytes count dowloaded for unfinished requests with an estimated size. + uint64_t unfinished_estimate_bytes_done; + // Size of the request with the most bytes on the page. + uint64_t largest_byte_size_known; +}; + +// RequestTrackerImpl captures and stores all the network requests that +// initiated from a particular tab. It only keeps the URLs and eventually, if +// available, the expected length of the result and the length of the received +// data so far as this is used to build a progress bar for a page. +// Note that the Request tracker has no notion of a page, it only tracks the +// requests by tab. In order for the tracker to know that a request is for a +// page or a subresource it is necessary for the tab to call StartPageLoad() +// with the URL of the page once it is known to avoid storing all the requests +// forever. +// +// The consumer needs to implement the CRWRequestTrackerImplDelegate protocol +// and needs to call StartPageLoad() and FinishPageLoad() to indicate the page +// boundaries. StartPageLoad() will also have the side effect of clearing past +// requests from memory. The consumer is assumed to be on the UI thread at all +// times. +// +// RequestTrackerImpl objects are created and destroyed on the UI thread and +// must be owned by some other object on the UI thread by way of a +// scoped_refptr, as returned by the public static constructor method, +// CreateTrackerForRequestGroupID. All consumer API methods will be called +// through this pointer. + +class RequestTrackerImpl; + +struct RequestTrackerImplTraits { + static void Destruct(const RequestTrackerImpl* t); +}; + +class RequestTrackerImpl + : public base::RefCountedThreadSafe<RequestTrackerImpl, + RequestTrackerImplTraits>, + public net::RequestTracker { + public: +#pragma mark Public Consumer API + // Consumer API methods should only be called on the UI thread. + + // Create a new RequestTrackerImpl associated with a particular tab. The + // profile must be the one associated to the given tab. This method has to be + // called *once* per tab and needs to be called before triggering any network + // request. The caller of CreateTrackerForRequestGroupID owns the tracker, and + // this class also keeps a global map of all active trackers. When the owning + // object releases it, the class removes it from the global map. + static scoped_refptr<RequestTrackerImpl> CreateTrackerForRequestGroupID( + NSString* request_group_id, + BrowserState* browser_state, + net::URLRequestContextGetter* context_getter, + id<CRWRequestTrackerDelegate> delegate); + + // The network layer has no way to know which network request is the primary + // one for a page load. The tab knows, either because it initiated the page + // load via the URL or received a callback informing it of the page change. + // Every time this happens the tab should call this method to clear the + // resources tracked. + // This will forget all the finished requests made before this URL in history. + // user_info is to be used by the consumer to store more additional specific + // info about the page, as an URL is not unique. + void StartPageLoad(const GURL& url, id user_info); + + // In order to properly provide progress information the tracker needs to know + // when the page is fully loaded. |load_success| indicates if the page + // successfully loaded. + void FinishPageLoad(const GURL& url, bool load_success); + + // Tells the tracker that history.pushState() or history.replaceState() + // changed the page URL. + void HistoryStateChange(const GURL& url); + + // Marks the tracker as closed. An owner must call this before the tracker is + // deleted. Once closed, no further calls will be made to the delegate. + void Close(); + + // Call |callback| on the UI thread after any pending request cancellations + // have completed on the IO thread. + // This should be used to delete a profile for which all of the trackers + // that use the profile's request context are closed. + static void RunAfterRequestsCancel(const base::Closure& callback); + + // Block until all pending IO thread activity has completed. This should only + // be used when Chrome is shutting down, and after all request trackers have + // had Close() called on them. + static void BlockUntilTrackersShutdown(); + +#pragma mark Client utility methods. + + // Finds the tracker given the tab ID. As calling this method involves a lock + // it is expected that the provider will call it only once. + // Returns a weak pointer, which should only be dereferenced on the IO thread. + // Returns NULL if no tracker exists for |request_group_id|. + static RequestTrackerImpl* GetTrackerForRequestGroupID( + NSString* request_group_id); + + // Callback from the UI to allow or deny a particular certificate. + void ErrorCallback(CRWSSLCarrier* carrier, bool allow); + + // Utility method for clients to post tasks to the IO thread from the UI + // thread. + void PostIOTask(const base::Closure& task); + + // Utility method for clients to post tasks to the IO thread from the IO + // thread. + void ScheduleIOTask(const base::Closure& task); + + // Utility method for clients to conditionally post tasks to the UI thread + // from the IO thread. The task will not be posted if the request tracker + // is in the process of closing (thus it "is open"). + void PostUITaskIfOpen(const base::Closure& task); + // Static version of the method, where |tracker| is a RequestTrackerImpl + // passed as a base::WeakPtr<RequestTracker>. + static void PostUITaskIfOpen(const base::WeakPtr<RequestTracker> tracker, + const base::Closure& task); + + // Sets the cache mode. Must be called from the UI thread. + void SetCacheModeFromUIThread(RequestTracker::CacheMode mode); + +#pragma mark Testing methods + + void SetCertificatePolicyCacheForTest(web::CertificatePolicyCache* cache); + +#pragma mark Accessors used by internal classes and network clients. + int identifier() { return identifier_; } + bool has_mixed_content() { return has_mixed_content_; } + + // RequestTracker implementation. + void StartRequest(net::URLRequest* request) override; + void CaptureHeaders(net::URLRequest* request) override; + void CaptureExpectedLength(const net::URLRequest* request, + uint64_t length) override; + void CaptureReceivedBytes(const net::URLRequest* request, + uint64_t byte_count) override; + void CaptureCertificatePolicyCache( + const net::URLRequest* request, + const SSLCallback& should_continue) override; + void StopRequest(net::URLRequest* request) override; + void StopRedirectedRequest(net::URLRequest* request) override; + void OnSSLCertificateError(const net::URLRequest* request, + const net::SSLInfo& ssl_info, + bool recoverable, + const SSLCallback& should_continue) override; + net::URLRequestContext* GetRequestContext() override; + + private: + friend class base::RefCountedThreadSafe<RequestTrackerImpl>; + friend struct RequestTrackerImplTraits; + +#pragma mark Object lifecycle API + // Private. RequestTrackerImpls are created through + // CreateTrackerForRequestGroupID(). + RequestTrackerImpl(NSString* request_group_id, + net::URLRequestContextGetter* context_getter, + id<CRWRequestTrackerDelegate> delegate); + + void InitOnIOThread( + const scoped_refptr<web::CertificatePolicyCache>& policy_cache); + + // Private destructor because the object is reference counted. A no-op; the + // useful destruction work happens in Destruct(). + ~RequestTrackerImpl() override; + + // Handles pre-destruction destruction tasks. This is invoked by + // RequestTrackerImplTraits::Destruct whenever the reference count of a + // RequestTrackerImpl is zero, and this will untimately delete the + // RequestTrackerImpl. + void Destruct(); + +#pragma mark Private Provider API + // Private methods that implement provider API features. All are only called + // on the IO thread. + + // Called when something has changed (network load progress or SSL status) + // that the consumer should know about. Notifications are asynchronous and + // batched. + void Notify(); + + // If no other notifications are pending, notifies the consumer of SSL status + // and load progress. + void StackNotification(); + + // Notify the consumer about the SSL status of this tracker's page load. + void SSLNotify(); + + // If the counts is for a request currently waiting for the user to approve it + // will reevaluate the approval. + void EvaluateSSLCallbackForCounts(TrackerCounts* counts); + + // Loop through all the requests waiting for approval and invoke + // |-evaluateSSLCallbackForCounts:| on all the ones with an |UNKNOWN| + // judgment. + void ReevaluateCallbacksForAllCounts(); + + // To cancel a rejected request due to a SSL issue. + void CancelRequestForCounts(TrackerCounts* counts); + + // Estimate the page load progress. Returns -1 if the progress didn't change + // since the last time this method was invoked. + float EstimatedProgress(); + + // The URL change notification is often late, therefore the mixed content + // status and the certificate policies may need to be recomputed. + void RecomputeMixedContent(const TrackerCounts* split_position); + void RecomputeCertificatePolicy(const TrackerCounts* split_position); + + // Remove all finished request up to the last instance of |url|. If url is not + // found, this will clear all the requests. + void TrimToURL(const GURL& url, id user_info); + + // Sets page_url_ to the new URL if it's a valid history state change (i.e. + // the URL's have the same origin) and if the tab is currently loading. + void HistoryStateChangeToURL(const GURL& full_url); + + // Note that the page started by a call to Trim is no longer loading. + // |load_success| indicates if the page successfully loaded. + void StopPageLoad(const GURL& url, bool load_success); + + // Cancels all the requests in |live_requests_|. + void CancelRequests(); + +#pragma mark Private Consumer API + // Private methods that call into delegate methods. + + // Notify* methods are posted to the UI thread by the provider API + // methods. + + // Has the delegate handle |headers| for |request_url|. + void NotifyResponseHeaders(net::HttpResponseHeaders* headers, + const GURL& request_url); + + // Notifies the deleage of certificate use. + void NotifyCertificateUsed(net::X509Certificate* certificate, + const std::string& host, + net::CertStatus status); + + // Notifies the deleate of a load completion estimate. + void NotifyUpdatedProgress(float estimate); + + // Has the delegate clear SSL certificates. + void NotifyClearCertificates(); + + // Notifies the delegate of an SSL status update. + void NotifyUpdatedSSLStatus(base::scoped_nsobject<CRWSSLCarrier> carrier); + + // Calls the delegate method to present an SSL error interstitial. + void NotifyPresentSSLError(base::scoped_nsobject<CRWSSLCarrier> carrier, + bool recoverable); + +#pragma mark Internal utilities for task posting + // Posts |task| to |thread|. Must not be called from |thread|. If |thread| is + // the IO thread, silently returns if |is_closing_| is true. + void PostTask(const base::Closure& task, web::WebThread::ID thread); + + // Posts |block| to |thread|, safely passing in |caller| to |block|. + void PostBlock(id caller, void (^block)(id), web::WebThread::ID thread); + +#pragma mark Other internal methods. + // Returns the current state of the page. + PageCounts pageCounts(); + + // Like description, but cannot be called from any thread. It must be called + // only from the IO thread. + NSString* UnsafeDescription(); + + // Generates a string unique to this RequestTrackerImpl to use with the + // CRWNetworkActivityIndicatorManager. + NSString* GetNetworkActivityKey(); + +#pragma mark Non thread-safe fields, only accessed from the main thread. + // The RequestTrackerImpl delegate. All changes and access to this object + // should be done on the main thread. + id<CRWRequestTrackerDelegate> delegate_; // Weak. + +#pragma mark Non thread-safe fields, only accessed from the IO thread. + // All the tracked requests for the page, indexed by net::URLRequest (Cast as + // a void* to avoid the temptation of accessing it from the wrong thread). + // This map is not exhaustive: it is only meant to estimate the loading + // progress, and thus requests corresponding to old navigation events are not + // in it. + std::map<const void*, TrackerCounts*> counts_by_request_; + // All the live requests associated with the tracker. + std::set<net::URLRequest*> live_requests_; + // A list of all the TrackerCounts, including the finished ones. + ScopedVector<TrackerCounts> counts_; + // The system shall never allow the page load estimate to go back. + float previous_estimate_; + // Index of the first request to consider for building the estimation. + unsigned int estimate_start_index_; + // How many notifications are currently queued, to avoid notifying too often. + int notification_depth_; + // The tracker containing the error currently presented to the user. + TrackerCounts* current_ssl_error_; + // Set to |YES| if the page has mixed content + bool has_mixed_content_; + // Set to true if between TrimToURL and StopPageLoad. + bool is_loading_; + // Set to true in TrimToURL if starting a new estimate round. Set to false by + // StartRequest once the new round is started. + bool new_estimate_round_; + +#pragma mark Other fields. + scoped_refptr<web::CertificatePolicyCache> policy_cache_; + // If |true| all the requests should be static file requests, otherwise all + // the requests should be network requests. This is a constant initialized + // in the constructor and read in IO and UI threads. + const bool is_for_static_file_requests_; + + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + // Current page URL, as far as we know. + GURL page_url_; + // Userinfo attached to the page, passed back by the delegate. + base::scoped_nsobject<id> user_info_; + // A tracker identifier (a simple increasing number) used to store + // certificates. + int identifier_; + // The string that identifies the tab this tracker serves. Used to index + // g_trackers. + base::scoped_nsobject<NSString> request_group_id_; + // Flag to synchronize deletion and callback creation. Lives on the IO thread. + // True when this tracker has beed Close()d. If this is the case, no further + // references to it should be generated (for example by binding it into a + // callback), and the expectation is that it will soon be deleted. + bool is_closing_; +}; + +} // namespace web + +#endif // IOS_WEB_NET_REQUEST_TRACKER_IMPL_H_ diff --git a/ios/web/net/request_tracker_impl.mm b/ios/web/net/request_tracker_impl.mm new file mode 100644 index 0000000..f274771 --- /dev/null +++ b/ios/web/net/request_tracker_impl.mm @@ -0,0 +1,1308 @@ +// Copyright 2014 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 "ios/web/net/request_tracker_impl.h" + +#include <pthread.h> + +#include "base/containers/hash_tables.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/mac/bind_objc_block.h" +#include "base/mac/scoped_nsobject.h" +#include "base/strings/string_util.h" +#include "base/strings/sys_string_conversions.h" +#include "base/synchronization/lock.h" +#import "ios/net/clients/crn_forwarding_network_client.h" +#import "ios/net/clients/crn_forwarding_network_client_factory.h" +#import "ios/web/crw_network_activity_indicator_manager.h" +#import "ios/web/history_state_util.h" +#import "ios/web/net/crw_request_tracker_delegate.h" +#include "ios/web/public/browser_state.h" +#include "ios/web/public/cert_store.h" +#include "ios/web/public/certificate_policy_cache.h" +#include "ios/web/public/ssl_status.h" +#include "ios/web/public/url_util.h" +#include "ios/web/public/web_thread.h" +#import "net/base/mac/url_conversions.h" +#include "net/base/net_errors.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request.h" + +namespace { + +struct EqualNSStrings { + bool operator()(const base::scoped_nsobject<NSString>& s1, + const base::scoped_nsobject<NSString>& s2) const { + // Use a ternary due to the BOOL vs bool type difference. + return [s1 isEqualToString:s2] ? true : false; + } +}; + +struct HashNSString { + size_t operator()(const base::scoped_nsobject<NSString>& s) const { + return [s hash]; + } +}; + +// A map of all RequestTrackerImpls for tabs that are: +// * Currently open +// * Recently closed waiting for all their network operations to finish. +// The code accesses this variable from two threads: the consumer is expected to +// always access it from the main thread, the provider is accessing it from the +// WebThread, a thread created by the UIWebView/CFURL. For this reason access to +// this variable must always gated by |g_trackers_lock|. +typedef base::hash_map<base::scoped_nsobject<NSString>, + web::RequestTrackerImpl*, + HashNSString, EqualNSStrings> TrackerMap; + +TrackerMap* g_trackers = NULL; +base::Lock* g_trackers_lock = NULL; +pthread_once_t g_once_control = PTHREAD_ONCE_INIT; + +// Flag, lock, and function to implement BlockUntilTrackersShutdown(). +// |g_waiting_on_io_thread| is guarded by |g_waiting_on_io_thread_lock|; +// it is set to true when the shutdown wait starts, then a call to +// StopIOThreadWaiting is posted to the IO thread (enqueued after any pending +// request terminations) while the posting method loops over a check on the +// |g_waiting_on_io_thread|. +static bool g_waiting_on_io_thread = false; +base::Lock* g_waiting_on_io_thread_lock = NULL; +void StopIOThreadWaiting() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + base::AutoLock scoped_lock(*g_waiting_on_io_thread_lock); + g_waiting_on_io_thread = false; +} + +// Initialize global state. Calls to this should be conditional on +// |g_once_control| (that is, this should only be called once, across all +// threads). +void InitializeGlobals() { + g_trackers = new TrackerMap; + g_trackers_lock = new base::Lock; + g_waiting_on_io_thread_lock = new base::Lock; +} + +// Each request tracker get a unique increasing number, used anywhere an +// identifier is needed for tracker (e.g. storing certs). +int g_next_request_tracker_id = 0; + +// IsIntranetHost logic and its associated kDot constant are lifted directly +// from content/browser/ssl/ssl_policy.cc. Unfortunately that particular file +// has way too many dependencies on content to be used on iOS. +static const char kDot = '.'; + +static bool IsIntranetHost(const std::string& host) { + const size_t dot = host.find(kDot); + return dot == std::string::npos || dot == host.length() - 1; +} + +// Add |tracker| to |g_trackers| under |key|. +static void RegisterTracker(web::RequestTrackerImpl* tracker, NSString* key) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + pthread_once(&g_once_control, &InitializeGlobals); + { + base::scoped_nsobject<NSString> scoped_key([key copy]); + base::AutoLock scoped_lock(*g_trackers_lock); + DCHECK(!g_trackers->count(scoped_key)); + (*g_trackers)[scoped_key] = tracker; + } +} + +// Empty callback. +void DoNothing(bool flag) {} + +} // namespace + +// The structure used to gather the information about the resources loaded. +struct TrackerCounts { + public: + TrackerCounts(const GURL& tracked_url, const net::URLRequest* tracked_request) + : url(tracked_url), + first_party_for_cookies_origin( + tracked_request->first_party_for_cookies().GetOrigin()), + request(tracked_request), + ssl_info(net::SSLInfo()), + ssl_judgment(web::CertPolicy::ALLOWED), + allowed_by_user(false), + expected_length(0), + processed(0), + done(false) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + is_subrequest = tracked_request->first_party_for_cookies().is_valid() && + tracked_request->url() != tracked_request->first_party_for_cookies(); + }; + + // The resource url. + const GURL url; + // The origin of the url of the top level document of the resource. This is + // used to ignore request coming from an old document when detecting mixed + // content. + const GURL first_party_for_cookies_origin; + // The request associated with this struct. As a void* to prevent access from + // the wrong thread. + const void* request; + // SSLInfo for the request. + net::SSLInfo ssl_info; + // Is the SSL request blocked waiting for user choice. + web::CertPolicy::Judgment ssl_judgment; + // True if |ssl_judgment| is ALLOWED as the result of a user choice. + bool allowed_by_user; + // block to call to cancel or authorize a blocked request. + net::RequestTracker::SSLCallback ssl_callback; + // If known, the expected length of the resource in bytes. + uint64_t expected_length; + // Number of bytes loaded so far. + uint64_t processed; + // Set to true is the resource is fully loaded. + bool done; + // Set to true if the request has a main request set. + bool is_subrequest; + + NSString* Description() { + NSString* spec = base::SysUTF8ToNSString(url.spec()); + NSString* status = nil; + if (done) { + status = [NSString stringWithFormat:@"\t-- Done -- (%04qu) bytes", + processed]; + } else if (!expected_length) { + status = [NSString stringWithFormat:@"\t>> Loading (%04qu) bytes", + processed]; + } else { + status = [NSString stringWithFormat:@"\t>> Loading (%04qu/%04qu)", + processed, expected_length]; + } + + NSString* ssl = @""; + if (ssl_info.is_valid()) { + NSString* subject = base::SysUTF8ToNSString( + ssl_info.cert.get()->subject().GetDisplayName()); + NSString* issuer = base::SysUTF8ToNSString( + ssl_info.cert.get()->issuer().GetDisplayName()); + + ssl = [NSString stringWithFormat: + @"\n\t\tcert for '%@' issued by '%@'", subject, issuer]; + + if (!net::IsCertStatusMinorError(ssl_info.cert_status)) { + ssl = [NSString stringWithFormat:@"%@ (status: %0xd)", + ssl, ssl_info.cert_status]; + } + } + return [NSString stringWithFormat:@"%@\n\t\t%@%@", status, spec, ssl]; + } + + DISALLOW_COPY_AND_ASSIGN(TrackerCounts); +}; + +// A SSL carrier is used to transport SSL information to the UI via its +// encapsulation in a block. Once the object is constructed all public methods +// can be called from any thread safely. This object is designed so it is +// instantiated on the IO thread but may be accessed from the UI thread. +@interface CRWSSLCarrier : NSObject { + @private + scoped_refptr<web::RequestTrackerImpl> tracker_; + net::SSLInfo sslInfo_; + GURL url_; + web::SSLStatus status_; +} + +// Designated initializer. +- (id)initWithTracker:(web::RequestTrackerImpl*)tracker + counts:(const TrackerCounts*)counts; +// URL of the request. +- (const GURL&)url; +// Returns a SSLStatus representing the state of the page. This assumes the +// target carrier is the main page request. +- (const web::SSLStatus&)sslStatus; +// Returns a SSLInfo with a reference to the certificate and SSL information. +- (const net::SSLInfo&)sslInfo; +// Callback method to allow or deny the request from going through. +- (void)errorCallback:(BOOL)flag; +// Internal method used to build the SSLStatus object. Called from the +// initializer to make sure it is invoked on the network thread. +- (void)buildSSLStatus; +@end + +@implementation CRWSSLCarrier + +- (id)initWithTracker:(web::RequestTrackerImpl*)tracker + counts:(const TrackerCounts*)counts { + self = [super init]; + if (self) { + tracker_ = tracker; + url_ = counts->url; + sslInfo_ = counts->ssl_info; + [self buildSSLStatus]; + } + return self; +} + +- (const GURL&)url { + return url_; +} + +- (const net::SSLInfo&)sslInfo { + return sslInfo_; +} + +- (const web::SSLStatus&)sslStatus { + return status_; +} + +- (void)errorCallback:(BOOL)flag { + base::scoped_nsobject<CRWSSLCarrier> scoped([self retain]); + web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, + base::Bind(&web::RequestTrackerImpl::ErrorCallback, + tracker_, scoped, flag)); +} + +- (void)buildSSLStatus { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + if (!sslInfo_.is_valid()) + return; + + status_.cert_id = web::CertStore::GetInstance()->StoreCert( + sslInfo_.cert.get(), tracker_->identifier()); + + status_.cert_status = sslInfo_.cert_status; + if (status_.cert_status & net::CERT_STATUS_COMMON_NAME_INVALID) { + // CAs issue certificates for intranet hosts to everyone. Therefore, we + // mark intranet hosts as being non-unique. + if (IsIntranetHost(url_.host())) { + status_.cert_status |= net::CERT_STATUS_NON_UNIQUE_NAME; + } + } + + status_.security_bits = sslInfo_.security_bits; + status_.connection_status = sslInfo_.connection_status; + + if (tracker_->has_mixed_content()) { + // TODO(noyau): In iOS there is no notion of resource type. The insecure + // content could be an image (DISPLAYED_INSECURE_CONTENT) or a script + // (RAN_INSECURE_CONTENT). The status of the page is different for both, but + // there is not enough information from UIWebView to differentiate the two + // cases. + status_.content_status = web::SSLStatus::DISPLAYED_INSECURE_CONTENT; + } else { + status_.content_status = web::SSLStatus::NORMAL_CONTENT; + } + + if (!url_.SchemeIsSecure()) { + // Should not happen as the sslInfo is valid. + NOTREACHED(); + status_.security_style = web::SECURITY_STYLE_UNAUTHENTICATED; + } else if (net::IsCertStatusError(status_.cert_status) && + !net::IsCertStatusMinorError(status_.cert_status)) { + // Minor errors don't lower the security style to + // SECURITY_STYLE_AUTHENTICATION_BROKEN. + status_.security_style = web::SECURITY_STYLE_AUTHENTICATION_BROKEN; + } else { + // This page is secure. + status_.security_style = web::SECURITY_STYLE_AUTHENTICATED; + } +} + +- (NSString*)description { + NSString* sslInfo = @""; + if (sslInfo_.is_valid()) { + switch (status_.security_style) { + case web::SECURITY_STYLE_UNKNOWN: + case web::SECURITY_STYLE_UNAUTHENTICATED: + sslInfo = @"Unexpected SSL state "; + break; + case web::SECURITY_STYLE_AUTHENTICATION_BROKEN: + sslInfo = @"Not secure "; + break; + case web::SECURITY_STYLE_AUTHENTICATED: + if (status_.content_status == + web::SSLStatus::DISPLAYED_INSECURE_CONTENT) + sslInfo = @"Mixed "; + else + sslInfo = @"Secure "; + break; + } + } + + NSURL* url = net::NSURLWithGURL(url_); + + return [NSString stringWithFormat:@"<%@%@>", sslInfo, url]; +} + +@end + +namespace web { + +#pragma mark Consumer API + +// static +scoped_refptr<RequestTrackerImpl> +RequestTrackerImpl::CreateTrackerForRequestGroupID( + NSString* request_group_id, + BrowserState* browser_state, + net::URLRequestContextGetter* context_getter, + id<CRWRequestTrackerDelegate> delegate) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + DCHECK(request_group_id); + + scoped_refptr<RequestTrackerImpl> tracker = + new RequestTrackerImpl(request_group_id, context_getter, delegate); + + scoped_refptr<CertificatePolicyCache> policy_cache = + BrowserState::GetCertificatePolicyCache(browser_state); + DCHECK(policy_cache); + + // Take care of the IO-thread init. + web::WebThread::PostTask( + web::WebThread::IO, FROM_HERE, + base::Bind(&RequestTrackerImpl::InitOnIOThread, tracker, policy_cache)); + RegisterTracker(tracker.get(), request_group_id); + return tracker; +} + +void RequestTrackerImpl::StartPageLoad(const GURL& url, id user_info) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + base::scoped_nsobject<id> scoped_user_info([user_info retain]); + web::WebThread::PostTask( + web::WebThread::IO, FROM_HERE, + base::Bind(&RequestTrackerImpl::TrimToURL, this, url, scoped_user_info)); +} + +void RequestTrackerImpl::FinishPageLoad(const GURL& url, bool load_success) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + web::WebThread::PostTask( + web::WebThread::IO, FROM_HERE, + base::Bind(&RequestTrackerImpl::StopPageLoad, this, url, load_success)); +} + +void RequestTrackerImpl::HistoryStateChange(const GURL& url) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + web::WebThread::PostTask( + web::WebThread::IO, FROM_HERE, + base::Bind(&RequestTrackerImpl::HistoryStateChangeToURL, this, url)); +} + +// Close is called when an owning object (a Tab or something that acts like +// it) is done with the RequestTrackerImpl. There may still be queued calls on +// the UI thread that will make use of the fields being cleaned-up here; they +// must ensure they they operate without crashing with the cleaned-up values. +void RequestTrackerImpl::Close() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + // Mark the tracker as closing on the IO thread. Note that because the local + // scoped_refptr here retains |this|, we a are guaranteed that destruiction + // won't begin until the block completes, and thus |is_closing_| will always + // be set before destruction begins. + scoped_refptr<RequestTrackerImpl> tracker = this; + web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, + base::BindBlock(^{ + tracker->is_closing_ = true; + tracker->CancelRequests(); + })); + + // Disable the delegate. + delegate_ = nil; + // The user_info is no longer needed. + user_info_.reset(); + // Get rid of the stored certificates + web::CertStore::GetInstance()->RemoveCertsForGroup(identifier_); +} + +// static +void RequestTrackerImpl::RunAfterRequestsCancel(const base::Closure& callback) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + // Post a no-op to the IO thread, and after that has executed, run |callback|. + // This ensures that |callback| runs after anything elese queued on the IO + // thread, in particular CancelRequest() calls made from closing trackers. + web::WebThread::PostTaskAndReply(web::WebThread::IO, FROM_HERE, + base::Bind(&base::DoNothing), callback); +} + +// static +void RequestTrackerImpl::BlockUntilTrackersShutdown() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + { + base::AutoLock scoped_lock(*g_waiting_on_io_thread_lock); + g_waiting_on_io_thread = true; + } + web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, + base::Bind(&StopIOThreadWaiting)); + + // Poll endlessly until the wait flag is unset on the IO thread by + // StopIOThreadWaiting(). + // (Consider instead having a hard time cap, like 100ms or so, after which + // we stop blocking. In that case this method would return a boolean + // indicating if the wait completed or not). + while (1) { + base::AutoLock scoped_lock(*g_waiting_on_io_thread_lock); + if (!g_waiting_on_io_thread) + return; + // Ensure that other threads have a chance to run even on a single-core + // devices. + pthread_yield_np(); + } +} + +#pragma mark Provider API + +// static +RequestTrackerImpl* RequestTrackerImpl::GetTrackerForRequestGroupID( + NSString* request_group_id) { + DCHECK(request_group_id); + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + RequestTrackerImpl* tracker = nullptr; + TrackerMap::iterator map_it; + pthread_once(&g_once_control, &InitializeGlobals); + { + base::AutoLock scoped_lock(*g_trackers_lock); + map_it = g_trackers->find( + base::scoped_nsobject<NSString>([request_group_id copy])); + if (map_it != g_trackers->end()) + tracker = map_it->second; + } + return tracker; +} + +net::URLRequestContext* RequestTrackerImpl::GetRequestContext() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + return request_context_getter_->GetURLRequestContext(); +} + +void RequestTrackerImpl::StartRequest(net::URLRequest* request) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + DCHECK(!counts_by_request_.count(request)); + DCHECK_EQ(is_for_static_file_requests_, request->url().SchemeIsFile()); + + bool addedRequest = live_requests_.insert(request).second; + if (!is_for_static_file_requests_ && addedRequest) { + NSString* networkActivityKey = GetNetworkActivityKey(); + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::BindBlock(^{ + [[CRWNetworkActivityIndicatorManager sharedInstance] + startNetworkTaskForGroup:networkActivityKey]; + })); + } + + if (new_estimate_round_) { + // Starting a new estimate round. Ignore the previous requests for the + // calculation. + counts_by_request_.clear(); + estimate_start_index_ = counts_.size(); + new_estimate_round_ = false; + } + const GURL& url = request->original_url(); + TrackerCounts* counts = new TrackerCounts( + GURLByRemovingRefFromGURL(url), request); + counts_.push_back(counts); + counts_by_request_[request] = counts; + if (page_url_.SchemeIsSecure() && !url.SchemeIsSecure()) + has_mixed_content_ = true; + Notify(); +} + +void RequestTrackerImpl::CaptureHeaders(net::URLRequest* request) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + if (is_closing_) + return; + + if (!request->response_headers()) + return; + + scoped_refptr<net::HttpResponseHeaders> headers(request->response_headers()); + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::Bind(&RequestTrackerImpl::NotifyResponseHeaders, this, headers, + request->url())); +} + +void RequestTrackerImpl::CaptureExpectedLength(const net::URLRequest* request, + uint64_t length) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + if (counts_by_request_.count(request)) { + TrackerCounts* counts = counts_by_request_[request]; + DCHECK(!counts->done); + if (length < counts->processed) { + // Something is wrong with the estimate. Ignore it. + counts->expected_length = 0; + } else { + counts->expected_length = length; + } + Notify(); + } +} + +void RequestTrackerImpl::CaptureReceivedBytes(const net::URLRequest* request, + uint64_t byte_count) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + if (counts_by_request_.count(request)) { + TrackerCounts* counts = counts_by_request_[request]; + DCHECK(!counts->done); + const net::SSLInfo& ssl_info = request->ssl_info(); + if (ssl_info.is_valid()) + counts->ssl_info = ssl_info; + counts->processed += byte_count; + if (counts->expected_length > 0 && + counts->expected_length < counts->processed) { + // Something is wrong with the estimate, it is too low. Ignore it. + counts->expected_length = 0; + } + Notify(); + } +} + +void RequestTrackerImpl::StopRequest(net::URLRequest* request) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + + int removedRequests = live_requests_.erase(request); + if (!is_for_static_file_requests_ && removedRequests > 0) { + NSString* networkActivityKey = GetNetworkActivityKey(); + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::BindBlock(^{ + [[CRWNetworkActivityIndicatorManager sharedInstance] + stopNetworkTaskForGroup:networkActivityKey]; + })); + } + + if (counts_by_request_.count(request)) { + StopRedirectedRequest(request); + Notify(); + } +} + +void RequestTrackerImpl::StopRedirectedRequest(net::URLRequest* request) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + + int removedRequests = live_requests_.erase(request); + if (!is_for_static_file_requests_ && removedRequests > 0) { + NSString* networkActivityKey = GetNetworkActivityKey(); + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::BindBlock(^{ + [[CRWNetworkActivityIndicatorManager sharedInstance] + stopNetworkTaskForGroup:networkActivityKey]; + })); + } + + if (counts_by_request_.count(request)) { + TrackerCounts* counts = counts_by_request_[request]; + DCHECK(!counts->done); + const net::SSLInfo& ssl_info = request->ssl_info(); + if (ssl_info.is_valid()) + counts->ssl_info = ssl_info; + counts->done = true; + counts_by_request_.erase(request); + } +} + +void RequestTrackerImpl::CaptureCertificatePolicyCache( + const net::URLRequest* request, + const RequestTracker::SSLCallback& should_continue) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + std::string host = request->url().host(); + CertPolicy::Judgment judgment = policy_cache_->QueryPolicy( + request->ssl_info().cert.get(), host, request->ssl_info().cert_status); + if (judgment == CertPolicy::UNKNOWN) { + // The request comes from the cache, and has been loaded even though the + // policy is UNKNOWN. Display the interstitial page now. + OnSSLCertificateError(request, request->ssl_info(), true, should_continue); + return; + } + + // Notify the delegate that a judgment has been used. + DCHECK(judgment == CertPolicy::ALLOWED); + if (counts_by_request_.count(request)) { + const net::SSLInfo& ssl_info = request->ssl_info(); + TrackerCounts* counts = counts_by_request_[request]; + counts->allowed_by_user = true; + if (ssl_info.is_valid()) + counts->ssl_info = ssl_info; + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::Bind(&RequestTrackerImpl::NotifyCertificateUsed, this, + ssl_info.cert, host, ssl_info.cert_status)); + } + should_continue.Run(true); +} + +void RequestTrackerImpl::OnSSLCertificateError( + const net::URLRequest* request, + const net::SSLInfo& ssl_info, + bool recoverable, + const RequestTracker::SSLCallback& should_continue) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + DCHECK(ssl_info.is_valid()); + + if (counts_by_request_.count(request)) { + TrackerCounts* counts = counts_by_request_[request]; + + DCHECK(!counts->done); + // Store the ssl error. + counts->ssl_info = ssl_info; + counts->ssl_callback = should_continue; + counts->ssl_judgment = + recoverable ? CertPolicy::UNKNOWN : CertPolicy::DENIED; + ReevaluateCallbacksForAllCounts(); + } +} + +void RequestTrackerImpl::ErrorCallback(CRWSSLCarrier* carrier, bool allow) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + DCHECK(policy_cache_); + + if (allow) { + policy_cache_->AllowCertForHost([carrier sslInfo].cert.get(), + [carrier url].host(), + [carrier sslInfo].cert_status); + ReevaluateCallbacksForAllCounts(); + } + current_ssl_error_ = NULL; +} + +#pragma mark Client utility methods. + +// TODO(marq): Convert all internal task-posting to use these. +void RequestTrackerImpl::PostUITaskIfOpen(const base::Closure& task) { + PostTask(task, web::WebThread::UI); +} + +// static +void RequestTrackerImpl::PostUITaskIfOpen( + const base::WeakPtr<RequestTracker> tracker, + const base::Closure& task) { + if (!tracker) + return; + RequestTrackerImpl* tracker_impl = + static_cast<RequestTrackerImpl*>(tracker.get()); + tracker_impl->PostUITaskIfOpen(task); +} + +void RequestTrackerImpl::PostIOTask(const base::Closure& task) { + PostTask(task, web::WebThread::IO); +} + +void RequestTrackerImpl::ScheduleIOTask(const base::Closure& task) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, task); +} + +void RequestTrackerImpl::SetCacheModeFromUIThread( + RequestTracker::CacheMode mode) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + web::WebThread::PostTask( + web::WebThread::IO, FROM_HERE, + base::Bind(&RequestTracker::SetCacheMode, this, mode)); +} + +#pragma mark Private Object Lifecycle API + +RequestTrackerImpl::RequestTrackerImpl( + NSString* request_group_id, + net::URLRequestContextGetter* context_getter, + id<CRWRequestTrackerDelegate> delegate) + : delegate_(delegate), + previous_estimate_(0.0f), // Not active by default. + estimate_start_index_(0), + notification_depth_(0), + current_ssl_error_(NULL), + has_mixed_content_(false), + is_loading_(false), + new_estimate_round_(true), + is_for_static_file_requests_([delegate isForStaticFileRequests]), + request_context_getter_(context_getter), + identifier_(++g_next_request_tracker_id), + request_group_id_([request_group_id copy]), + is_closing_(false) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); +} + +void RequestTrackerImpl::InitOnIOThread( + const scoped_refptr<CertificatePolicyCache>& policy_cache) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + Init(); + DCHECK(policy_cache); + policy_cache_ = policy_cache; +} + +RequestTrackerImpl::~RequestTrackerImpl() { +} + +void RequestTrackerImplTraits::Destruct(const RequestTrackerImpl* t) { + // RefCountedThreadSafe assumes we can do all the destruct tasks with a + // const pointer, but we actually can't. + RequestTrackerImpl* inconstant_t = const_cast<RequestTrackerImpl*>(t); + if (web::WebThread::CurrentlyOn(web::WebThread::IO)) { + inconstant_t->Destruct(); + } else { + // Use BindBlock rather than Bind to avoid creating another scoped_refpter + // to |this|. |inconstant_t| isn't retained by the block, but since this + // method is the mechanism by which all RequestTrackerImpl instances are + // destroyed, the object inconstant_t points to won't be deleted while + // the block is executing (and Destruct() itself will do the deleting). + web::WebThread::PostTask(web::WebThread::IO, FROM_HERE, + base::BindBlock(^{ + inconstant_t->Destruct(); + })); + } +} + +void RequestTrackerImpl::Destruct() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + DCHECK(is_closing_); + + pthread_once(&g_once_control, &InitializeGlobals); + { + base::AutoLock scoped_lock(*g_trackers_lock); + g_trackers->erase(request_group_id_); + } + InvalidateWeakPtrs(); + // Delete on the UI thread. + web::WebThread::PostTask(web::WebThread::UI, FROM_HERE, base::BindBlock(^{ + delete this; + })); +} + +#pragma mark Other private methods +// TODO(marq): Reorder method implementations to match header and add grouping +// marks/comments. + +void RequestTrackerImpl::Notify() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + if (is_closing_) + return; + // Notify() is called asynchronously, it runs later on the same + // thread. This is used to collate notifications together, avoiding + // blanketing the UI with a stream of information. + notification_depth_ += 1; + web::WebThread::PostTask( + web::WebThread::IO, FROM_HERE, + base::Bind(&RequestTrackerImpl::StackNotification, this)); +} + +void RequestTrackerImpl::StackNotification() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + if (is_closing_) + return; + + // There is no point in sending the notification if there is another one + // already queued. This queue is processing very lightweight changes and + // should be exhausted very easily. + --notification_depth_; + if (notification_depth_) + return; + + SSLNotify(); + if (is_loading_) { + float estimate = EstimatedProgress(); + if (estimate != -1.0f) { + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::Bind(&RequestTrackerImpl::NotifyUpdatedProgress, this, + estimate)); + } + } +} + +void RequestTrackerImpl::SSLNotify() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + if (is_closing_) + return; + + if (!counts_.size()) + return; // Nothing yet to notify. + + if (!page_url_.SchemeIsSecure()) + return; + + const GURL page_origin = page_url_.GetOrigin(); + ScopedVector<TrackerCounts>::iterator it; + for (it = counts_.begin(); it != counts_.end(); ++it) { + if (!(*it)->ssl_info.is_valid()) + continue; // No SSL info at this point in time on this tracker. + + GURL request_origin = (*it)->url.GetOrigin(); + if (request_origin != page_origin) + continue; // Not interesting in the context of the page. + + base::scoped_nsobject<CRWSSLCarrier> carrier( + [[CRWSSLCarrier alloc] initWithTracker:this counts:*it]); + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::Bind(&RequestTrackerImpl::NotifyUpdatedSSLStatus, this, carrier)); + break; + } +} + +void RequestTrackerImpl::NotifyResponseHeaders( + net::HttpResponseHeaders* headers, + const GURL& request_url) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + [delegate_ handleResponseHeaders:headers requestUrl:request_url]; +} + +void RequestTrackerImpl::NotifyCertificateUsed( + net::X509Certificate* certificate, + const std::string& host, + net::CertStatus status) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + [delegate_ certificateUsed:certificate forHost:host status:status]; +} + +void RequestTrackerImpl::NotifyUpdatedProgress(float estimate) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + [delegate_ updatedProgress:estimate]; +} + +void RequestTrackerImpl::NotifyClearCertificates() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + [delegate_ clearCertificates]; +} + +void RequestTrackerImpl::NotifyUpdatedSSLStatus( + base::scoped_nsobject<CRWSSLCarrier> carrier) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + [delegate_ updatedSSLStatus:[carrier sslStatus] + forPageUrl:[carrier url] + userInfo:user_info_]; +} + +void RequestTrackerImpl::NotifyPresentSSLError( + base::scoped_nsobject<CRWSSLCarrier> carrier, + bool recoverable) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + [delegate_ presentSSLError:[carrier sslInfo] + forSSLStatus:[carrier sslStatus] + onUrl:[carrier url] + recoverable:recoverable + callback:^(BOOL flag) { + [carrier errorCallback:flag && recoverable]; + }]; +} + +void RequestTrackerImpl::EvaluateSSLCallbackForCounts(TrackerCounts* counts) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + DCHECK(policy_cache_); + + // Ignore non-SSL requests. + if (!counts->ssl_info.is_valid()) + return; + + CertPolicy::Judgment judgment = + policy_cache_->QueryPolicy(counts->ssl_info.cert.get(), + counts->url.host(), + counts->ssl_info.cert_status); + + if (judgment != CertPolicy::ALLOWED) { + // Apply some fine tuning. + // TODO(droger): This logic is duplicated from SSLPolicy. Sharing the code + // would be better. + switch (net::MapCertStatusToNetError(counts->ssl_info.cert_status)) { + case net::ERR_CERT_NO_REVOCATION_MECHANISM: + // Ignore this error. + judgment = CertPolicy::ALLOWED; + break; + case net::ERR_CERT_UNABLE_TO_CHECK_REVOCATION: + // We ignore this error but it will show a warning status in the + // location bar. + judgment = CertPolicy::ALLOWED; + break; + case net::ERR_CERT_CONTAINS_ERRORS: + case net::ERR_CERT_REVOKED: + case net::ERR_CERT_INVALID: + case net::ERR_SSL_WEAK_SERVER_EPHEMERAL_DH_KEY: + case net::ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN: + judgment = CertPolicy::DENIED; + break; + case net::ERR_CERT_WEAK_SIGNATURE_ALGORITHM: + case net::ERR_CERT_COMMON_NAME_INVALID: + case net::ERR_CERT_DATE_INVALID: + case net::ERR_CERT_AUTHORITY_INVALID: + // Nothing. If DENIED it will stay denied. If UNKNOWN it will be + // shown to the user for decision. + break; + default: + NOTREACHED(); + judgment = CertPolicy::DENIED; + break; + } + } + + counts->ssl_judgment = judgment; + + switch (judgment) { + case CertPolicy::UNKNOWN: + case CertPolicy::DENIED: + // Simply cancel the request. + CancelRequestForCounts(counts); + break; + case CertPolicy::ALLOWED: + counts->ssl_callback.Run(YES); + counts->ssl_callback = base::Bind(&DoNothing); + break; + default: + NOTREACHED(); + // For now simply cancel the request. + CancelRequestForCounts(counts); + break; + } +} + +void RequestTrackerImpl::ReevaluateCallbacksForAllCounts() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + if (is_closing_) + return; + + ScopedVector<TrackerCounts>::iterator it; + for (it = counts_.begin(); it != counts_.end(); ++it) { + // Check if the value hasn't changed via a user action. + if ((*it)->ssl_judgment == CertPolicy::UNKNOWN) + EvaluateSSLCallbackForCounts(*it); + + CertPolicy::Judgment judgment = (*it)->ssl_judgment; + if (judgment == CertPolicy::ALLOWED) + continue; + + // SSL errors on subrequests are simply ignored. The call to + // EvaluateSSLCallbackForCounts() cancelled the request and nothing will + // restart it. + if ((*it)->is_subrequest) + continue; + + if (!current_ssl_error_) { + // For the UNKNOWN and DENIED state the information should be pushed to + // the delegate. But only one at a time. + current_ssl_error_ = (*it); + base::scoped_nsobject<CRWSSLCarrier> carrier([[CRWSSLCarrier alloc] + initWithTracker:this counts:current_ssl_error_]); + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::Bind(&RequestTrackerImpl::NotifyPresentSSLError, this, carrier, + judgment == CertPolicy::UNKNOWN)); + } + } +} + +void RequestTrackerImpl::CancelRequestForCounts(TrackerCounts* counts) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + // Cancel the request. + counts->done = true; + counts_by_request_.erase(counts->request); + counts->ssl_callback.Run(NO); + counts->ssl_callback = base::Bind(&DoNothing); + Notify(); +} + +PageCounts RequestTrackerImpl::pageCounts() { + DCHECK_GE(counts_.size(), estimate_start_index_); + + PageCounts page_counts; + + ScopedVector<TrackerCounts>::iterator it; + for (it = counts_.begin() + estimate_start_index_; + it != counts_.end(); ++it) { + if ((*it)->done) { + uint64_t size = (*it)->processed; + page_counts.finished += 1; + page_counts.finished_bytes += size; + if (page_counts.largest_byte_size_known < size) { + page_counts.largest_byte_size_known = size; + } + } else { + page_counts.unfinished += 1; + if ((*it)->expected_length) { + uint64_t size = (*it)->expected_length; + page_counts.unfinished_estimate_bytes_done += (*it)->processed; + page_counts.unfinished_estimated_bytes_left += size; + if (page_counts.largest_byte_size_known < size) { + page_counts.largest_byte_size_known = size; + } + } else { + page_counts.unfinished_no_estimate += 1; + page_counts.unfinished_no_estimate_bytes_done += (*it)->processed; + } + } + } + + return page_counts; +} + +float RequestTrackerImpl::EstimatedProgress() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + + const PageCounts page_counts = pageCounts(); + + // Nothing in progress and the last time was the same. + if (!page_counts.unfinished && previous_estimate_ == 0.0f) + return -1.0f; + + // First request. + if (previous_estimate_ == 0.0f) { + // start low. + previous_estimate_ = 0.1f; + return previous_estimate_; // Return the just started status. + } + + // The very simple case where everything is probably done and dusted. + if (!page_counts.unfinished) { + // Add 60%, and return. Another task is going to finish this. + float bump = (1.0f - previous_estimate_) * 0.6f; + previous_estimate_ += bump; + return previous_estimate_; + } + + // Calculate some ratios. + // First the ratio of the finished vs the unfinished counts of resources + // loaded. + float unfinishedRatio = + static_cast<float>(page_counts.finished) / + static_cast<float>(page_counts.unfinished + page_counts.finished); + + // The ratio of bytes left vs bytes already downloaded for the resources where + // no estimates of final size are known. For this ratio it is assumed the size + // of a resource not downloaded yet is the maximum size of all the resources + // seen so far. + float noEstimateRatio = (!page_counts.unfinished_no_estimate_bytes_done) ? + 0.0f : + static_cast<float>(page_counts.unfinished_no_estimate * + page_counts.largest_byte_size_known) / + static_cast<float>(page_counts.finished_bytes + + page_counts.unfinished_no_estimate_bytes_done); + + // The ratio of bytes left vs bytes already downloaded for the resources with + // available estimated size. + float estimateRatio = (!page_counts.unfinished_estimated_bytes_left) ? + noEstimateRatio : + static_cast<float>(page_counts.unfinished_estimate_bytes_done) / + static_cast<float>(page_counts.unfinished_estimate_bytes_done + + page_counts.unfinished_estimated_bytes_left); + + // Reassemble all of this. + float total = + 0.1f + // Minimum value. + unfinishedRatio * 0.6f + + estimateRatio * 0.3f; + + if (previous_estimate_ >= total) + return -1.0f; + + // 10% of what's left. + float maxBump = (1.0f - previous_estimate_) / 10.0f; + // total is greater than previous estimate, need to bump the estimate up. + if ((previous_estimate_ + maxBump) > total) { + // Less than a 10% bump, bump to the new value. + previous_estimate_ = total; + } else { + // Just bump by 10% toward the total. + previous_estimate_ += maxBump; + } + + return previous_estimate_; +} + +void RequestTrackerImpl::RecomputeMixedContent( + const TrackerCounts* split_position) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + // Check if the mixed content before trimming was correct. + if (page_url_.SchemeIsSecure() && has_mixed_content_) { + bool old_url_has_mixed_content = false; + const GURL origin = page_url_.GetOrigin(); + ScopedVector<TrackerCounts>::iterator it = counts_.begin(); + while (it != counts_.end() && *it != split_position) { + if (!(*it)->url.SchemeIsSecure() && + origin == (*it)->first_party_for_cookies_origin) { + old_url_has_mixed_content = true; + break; + } + ++it; + } + if (!old_url_has_mixed_content) { + // We marked the previous page with incorrect data about its mixed + // content. Turns out that the elements that triggered that condition + // where in fact in a subsequent page. Duh. + // Resend a notification for the |page_url_| informing the upper layer + // that the mixed content was a red herring. + has_mixed_content_ = false; + SSLNotify(); + } + } +} + +void RequestTrackerImpl::RecomputeCertificatePolicy( + const TrackerCounts* splitPosition) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + // Clear the judgments for the old URL. + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::Bind(&RequestTrackerImpl::NotifyClearCertificates, this)); + // Report judgements for the new URL. + ScopedVector<TrackerCounts>::const_reverse_iterator it; + for (it = counts_.rbegin(); it != counts_.rend(); ++it) { + TrackerCounts* counts = *it; + if (counts->allowed_by_user) { + std::string host = counts->url.host(); + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::Bind(&RequestTrackerImpl::NotifyCertificateUsed, this, + counts->ssl_info.cert, host, + counts->ssl_info.cert_status)); + } + if (counts == splitPosition) + break; + } +} + +void RequestTrackerImpl::HistoryStateChangeToURL(const GURL& full_url) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + GURL url = GURLByRemovingRefFromGURL(full_url); + + if (is_loading_ && + web::history_state_util::IsHistoryStateChangeValid(url, page_url_)) { + page_url_ = url; + } +} + +void RequestTrackerImpl::TrimToURL(const GURL& full_url, id user_info) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + + GURL url = GURLByRemovingRefFromGURL(full_url); + + // Locate the request with this url, if present. + bool new_url_has_mixed_content = false; + bool url_scheme_is_secure = url.SchemeIsSecure(); + ScopedVector<TrackerCounts>::const_reverse_iterator rit = counts_.rbegin(); + while (rit != counts_.rend() && (*rit)->url != url) { + if (url_scheme_is_secure && !(*rit)->url.SchemeIsSecure() && + (*rit)->first_party_for_cookies_origin == url.GetOrigin()) { + new_url_has_mixed_content = true; + } + ++rit; + } + + // |split_position| will be set to the count for the passed url if it exists. + TrackerCounts* split_position = NULL; + if (rit != counts_.rend()) { + split_position = (*rit); + } else { + // The URL was not found, everything will be trimmed. The mixed content + // calculation is invalid. + new_url_has_mixed_content = false; + + // In the case of a page loaded via a HTML5 manifest there is no page + // boundary to be found. However the latest count is a request for a + // manifest. This tries to detect this peculiar case. + // This is important as if this request for the manifest is on the same + // domain as the page itself this will allow retrieval of the SSL + // information. + if (url_scheme_is_secure && counts_.size()) { + TrackerCounts* back = counts_.back(); + const GURL& back_url = back->url; + if (back_url.SchemeIsSecure() && + back_url.GetOrigin() == url.GetOrigin() && + !back->is_subrequest) { + split_position = back; + } + } + } + RecomputeMixedContent(split_position); + RecomputeCertificatePolicy(split_position); + + // Trim up to that element. + ScopedVector<TrackerCounts>::iterator it = counts_.begin(); + while (it != counts_.end() && *it != split_position) { + if (!(*it)->done) { + // This is for an unfinished request on a previous page. We do not care + // about those anymore. Cancel the request. + if ((*it)->ssl_judgment == CertPolicy::UNKNOWN) + CancelRequestForCounts(*it); + counts_by_request_.erase((*it)->request); + } + it = counts_.erase(it); + } + + has_mixed_content_ = new_url_has_mixed_content; + page_url_ = url; + user_info_.reset([user_info retain]); + estimate_start_index_ = 0; + is_loading_ = true; + previous_estimate_ = 0.0f; + new_estimate_round_ = true; + ReevaluateCallbacksForAllCounts(); + Notify(); +} + +void RequestTrackerImpl::StopPageLoad(const GURL& url, bool load_success) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + DCHECK(page_url_ == GURLByRemovingRefFromGURL(url)); + is_loading_ = false; +} + +#pragma mark Internal utilities for task posting + +void RequestTrackerImpl::PostTask(const base::Closure& task, + web::WebThread::ID thread) { + // Absolute sanity test: |thread| is one of {UI, IO} + DCHECK(thread == web::WebThread::UI || thread == web::WebThread::IO); + // Check that we're on the counterpart thread to the one we're posting to. + DCHECK_CURRENTLY_ON_WEB_THREAD( + thread == web::WebThread::IO ? web::WebThread::UI : web::WebThread::IO); + // Don't post if the tracker is closing and we're on the IO thread. + // (there should be no way to call anything from the UI thread if + // the tracker is closing). + if (is_closing_ && web::WebThread::CurrentlyOn(web::WebThread::IO)) + return; + web::WebThread::PostTask(thread, FROM_HERE, task); +} + +#pragma mark Other internal methods. + +NSString* RequestTrackerImpl::UnsafeDescription() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + + NSMutableArray* urls = [NSMutableArray array]; + ScopedVector<TrackerCounts>::iterator it; + for (it = counts_.begin(); it != counts_.end(); ++it) + [urls addObject:(*it)->Description()]; + + return [NSString stringWithFormat:@"RequestGroupID %@\n%@\n%@", + request_group_id_.get(), + net::NSURLWithGURL(page_url_), + [urls componentsJoinedByString:@"\n"]]; +} + +NSString* RequestTrackerImpl::GetNetworkActivityKey() { + return [NSString + stringWithFormat:@"RequestTrackerImpl.NetworkActivityIndicatorKey.%@", + request_group_id_.get()]; + } + +void RequestTrackerImpl::CancelRequests() { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::IO); + std::set<net::URLRequest*>::iterator it; + // TODO(droger): When canceling the request, we should in theory make sure + // that the NSURLProtocol client method |didFailWithError| is called, + // otherwise the iOS system may wait indefinitely for the request to complete. + // However, as we currently only cancel the requests when closing a tab, the + // requests are all canceled by the system shortly after and nothing bad + // happens. + for (it = live_requests_.begin(); it != live_requests_.end(); ++it) + (*it)->Cancel(); + + int removedRequests = live_requests_.size(); + live_requests_.clear(); + if (!is_for_static_file_requests_ && removedRequests > 0) { + NSString* networkActivityKey = GetNetworkActivityKey(); + web::WebThread::PostTask( + web::WebThread::UI, FROM_HERE, + base::BindBlock(^{ + [[CRWNetworkActivityIndicatorManager sharedInstance] + clearNetworkTasksForGroup:networkActivityKey]; + })); + } +} + +void RequestTrackerImpl::SetCertificatePolicyCacheForTest( + web::CertificatePolicyCache* cache) { + policy_cache_ = cache; +} + +} // namespace web diff --git a/ios/web/net/request_tracker_impl_unittest.mm b/ios/web/net/request_tracker_impl_unittest.mm new file mode 100644 index 0000000..bb87192 --- /dev/null +++ b/ios/web/net/request_tracker_impl_unittest.mm @@ -0,0 +1,498 @@ +// Copyright 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 "ios/web/net/request_tracker_impl.h" + +#include "base/logging.h" +#include "base/mac/scoped_nsobject.h" +#include "base/memory/scoped_vector.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/sys_string_conversions.h" +#include "ios/web/public/cert_policy.h" +#include "ios/web/public/certificate_policy_cache.h" +#include "ios/web/public/ssl_status.h" +#include "ios/web/public/test/test_browser_state.h" +#include "ios/web/public/test/test_web_thread.h" +#include "net/cert/x509_certificate.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_job_factory.h" +#include "net/url_request/url_request_job_factory_impl.h" +#include "net/url_request/url_request_test_job.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" +#include "testing/platform_test.h" +#import "third_party/ocmock/OCMock/OCMock.h" +#include "third_party/ocmock/gtest_support.h" + +@interface RequestTrackerNotificationReceiverTest + : NSObject<CRWRequestTrackerDelegate> { + @public + float value_; + float max_; + @private + base::scoped_nsobject<NSString> error_; + scoped_refptr<net::HttpResponseHeaders> headers_; +} + +- (NSString*)error; +- (net::HttpResponseHeaders*)headers; +@end + +@implementation RequestTrackerNotificationReceiverTest + +- (id)init { + self = [super init]; + if (self) { + value_ = 0.0f; + max_ = 0.0f; + } + return self; +} + +- (BOOL)isForStaticFileRequests { + return NO; +} + +- (void)updatedProgress:(float)progress { + if (progress > 0.0f) { + if (progress < value_) { + error_.reset( + [[NSString stringWithFormat: + @"going down from %f to %f", value_, progress] retain]); + } + value_ = progress; + } else { + value_ = 0.0f; + } + if (value_ > max_) { + max_ = value_; + } +} + +- (NSString*)error { + return error_; +} + +- (void)handleResponseHeaders:(net::HttpResponseHeaders*)headers + requestUrl:(const GURL&)requestUrl { + headers_ = headers; +} + +- (net::HttpResponseHeaders*)headers { + return headers_.get(); +} + +- (void)updatedSSLStatus:(const web::SSLStatus&)sslStatus + forPageUrl:(const GURL&)url + userInfo:(id)userInfo { + // Nothing. yet. +} + +- (void)presentSSLError:(const net::SSLInfo&)info + forSSLStatus:(const web::SSLStatus&)status + onUrl:(const GURL&)url + recoverable:(BOOL)recoverable + callback:(SSLErrorCallback)shouldContinue { + // Nothing, yet. +} + +- (void)certificateUsed:(net::X509Certificate*)certificate + forHost:(const std::string&)host + status:(net::CertStatus)status { + // Nothing, yet. +} + +- (void)clearCertificates { + // Nothing, yet. +} + +- (void)handlePassKitObject:(NSData*)data { + // Nothing yet. +} + +@end + +namespace { + +// Used and incremented each time a tabId is created. +int g_count = 0; + +class RequestTrackerTest : public PlatformTest { + public: + RequestTrackerTest() + : loop_(base::MessageLoop::TYPE_IO), + ui_thread_(web::WebThread::UI, &loop_), + io_thread_(web::WebThread::IO, &loop_){}; + + ~RequestTrackerTest() override {} + + void SetUp() override { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + request_group_id_.reset( + [[NSString stringWithFormat:@"test%d", g_count++] retain]); + + receiver_.reset([[RequestTrackerNotificationReceiverTest alloc] init]); + tracker_ = web::RequestTrackerImpl::CreateTrackerForRequestGroupID( + request_group_id_, + &browser_state_, + browser_state_.GetRequestContext(), + receiver_); + } + + void TearDown() override { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + tracker_->Close(); + } + + base::MessageLoop loop_; + web::TestWebThread ui_thread_; + web::TestWebThread io_thread_; + + base::scoped_nsobject<RequestTrackerNotificationReceiverTest> receiver_; + scoped_refptr<web::RequestTrackerImpl> tracker_; + base::scoped_nsobject<NSString> request_group_id_; + web::TestBrowserState browser_state_; + ScopedVector<net::URLRequestContext> contexts_; + ScopedVector<net::URLRequest> requests_; + net::URLRequestJobFactoryImpl job_factory_; + + GURL GetURL(size_t i) { + std::stringstream ss; + ss << "http://www/"; + ss << i; + return GURL(ss.str()); + } + + GURL GetSecureURL(size_t i) { + std::stringstream ss; + ss << "https://www/"; + ss << i; + return GURL(ss.str()); + } + + net::URLRequest* GetRequest(size_t i) { + return GetInternalRequest(i, false); + } + + net::URLRequest* GetSecureRequest(size_t i) { + return GetInternalRequest(i, true); + } + + NSString* WaitUntilLoop(bool (^condition)(void)) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + base::Time maxDate = base::Time::Now() + base::TimeDelta::FromSeconds(10); + while (!condition()) { + if ([receiver_ error]) + return [receiver_ error]; + if (base::Time::Now() > maxDate) + return @"Time is up, too slow to go"; + loop_.RunUntilIdle(); + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(1)); + } + return nil; + } + + NSString* CheckActive() { + NSString* message = WaitUntilLoop(^{ + return (receiver_.get()->value_ > 0.0f); + }); + + if (!message && (receiver_.get()->max_ == 0.0f)) + message = @"Max should be greater than 0.0"; + return message; + } + + void TrimRequest(NSString* tab_id, const GURL& url) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + receiver_.get()->value_ = 0.0f; + receiver_.get()->max_ = 0.0f; + tracker_->StartPageLoad(url, nil); + } + + void EndPage(NSString* tab_id, const GURL& url) { + DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); + tracker_->FinishPageLoad(url, false); + receiver_.get()->value_ = 0.0f; + receiver_.get()->max_ = 0.0f; + loop_.RunUntilIdle(); + } + + net::TestJobInterceptor* AddInterceptorToRequest(size_t i) { + // |interceptor| will be deleted from |job_factory_|'s destructor. + net::TestJobInterceptor* protocol_handler = new net::TestJobInterceptor(); + job_factory_.SetProtocolHandler("http", protocol_handler); + contexts_[i]->set_job_factory(&job_factory_); + return protocol_handler; + } + + private: + net::URLRequest* GetInternalRequest(size_t i, bool secure) { + GURL url; + if (secure) + url = GetSecureURL(requests_.size()); + else + url = GetURL(requests_.size()); + + while (i >= requests_.size()) { + contexts_.push_back(new net::URLRequestContext()); + requests_.push_back(contexts_[i]->CreateRequest(url, + net::DEFAULT_PRIORITY, + NULL).release()); + + if (secure) { + // Put a valid SSLInfo inside + net::HttpResponseInfo* response = + const_cast<net::HttpResponseInfo*>(&requests_[i]->response_info()); + + response->ssl_info.cert = new net::X509Certificate( + "subject", "issuer", + base::Time::Now() - base::TimeDelta::FromDays(2), + base::Time::Now() + base::TimeDelta::FromDays(2)); + response->ssl_info.cert_status = 0; // No errors. + response->ssl_info.security_bits = 128; + + EXPECT_TRUE(requests_[i]->ssl_info().is_valid()); + } + } + EXPECT_TRUE(!secure == !requests_[i]->url().SchemeIsSecure()); + return requests_[i]; + } + + DISALLOW_COPY_AND_ASSIGN(RequestTrackerTest); +}; + +TEST_F(RequestTrackerTest, OnePage) { + // Start a request. + tracker_->StartRequest(GetRequest(0)); + // Start page load. + TrimRequest(request_group_id_, GetURL(0)); + EXPECT_NSEQ(nil, CheckActive()); + + // Stop the request. + tracker_->StopRequest(GetRequest(0)); + EndPage(request_group_id_, GetURL(0)); +} + +TEST_F(RequestTrackerTest, OneSecurePage) { + net::URLRequest* request = GetSecureRequest(0); + GURL url = GetSecureURL(0); + + // Start a page. + TrimRequest(request_group_id_, url); + + // Start a request. + tracker_->StartRequest(request); + tracker_->CaptureReceivedBytes(request, 42); + EXPECT_NSEQ(nil, CheckActive()); + + // Stop the request. + tracker_->StopRequest(request); + + EndPage(request_group_id_, url); +} + +TEST_F(RequestTrackerTest, OnePageAndResources) { + // Start a page. + TrimRequest(request_group_id_, GetURL(0)); + // Start two requests. + tracker_->StartRequest(GetRequest(0)); + tracker_->StartRequest(GetRequest(1)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->StopRequest(GetRequest(0)); + tracker_->StartRequest(GetRequest(2)); + EXPECT_NSEQ(nil, CheckActive()); + tracker_->StopRequest(GetRequest(1)); + tracker_->StartRequest(GetRequest(3)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->StopRequest(GetRequest(2)); + tracker_->StartRequest(GetRequest(4)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->StopRequest(GetRequest(3)); + tracker_->StopRequest(GetRequest(4)); + EndPage(request_group_id_, GetURL(0)); +} + +TEST_F(RequestTrackerTest, OnePageOneBigImage) { + // Start a page. + TrimRequest(request_group_id_, GetURL(0)); + tracker_->StartRequest(GetRequest(0)); + tracker_->StopRequest(GetRequest(0)); + tracker_->StartRequest(GetRequest(1)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->CaptureExpectedLength(GetRequest(1), 100); + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->CaptureReceivedBytes(GetRequest(1), 10); + tracker_->StopRequest(GetRequest(1)); + EndPage(request_group_id_, GetURL(0)); +} + +TEST_F(RequestTrackerTest, TwoPagesPostStart) { + tracker_->StartRequest(GetRequest(0)); + + TrimRequest(request_group_id_, GetURL(0)); + EXPECT_NSEQ(nil, CheckActive()); + tracker_->StartRequest(GetRequest(1)); + tracker_->StartRequest(GetRequest(2)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->StopRequest(GetRequest(0)); + tracker_->StopRequest(GetRequest(1)); + tracker_->StopRequest(GetRequest(2)); + EndPage(request_group_id_, GetURL(0)); + + tracker_->StartRequest(GetRequest(3)); + + TrimRequest(request_group_id_, GetURL(3)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->StopRequest(GetRequest(3)); + EndPage(request_group_id_, GetURL(3)); +} + +TEST_F(RequestTrackerTest, TwoPagesPreStart) { + tracker_->StartRequest(GetRequest(0)); + + TrimRequest(request_group_id_, GetURL(0)); + EXPECT_NSEQ(nil, CheckActive()); + tracker_->StartRequest(GetRequest(1)); + tracker_->StartRequest(GetRequest(2)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->StopRequest(GetRequest(0)); + tracker_->StopRequest(GetRequest(1)); + tracker_->StopRequest(GetRequest(2)); + EndPage(request_group_id_, GetURL(0)); + + TrimRequest(request_group_id_, GetURL(3)); + tracker_->StartRequest(GetRequest(3)); + tracker_->StopRequest(GetRequest(3)); + EndPage(request_group_id_, GetURL(3)); +} + +TEST_F(RequestTrackerTest, TwoPagesNoWait) { + tracker_->StartRequest(GetRequest(0)); + + TrimRequest(request_group_id_, GetURL(0)); + EXPECT_NSEQ(nil, CheckActive()); + tracker_->StartRequest(GetRequest(1)); + tracker_->StartRequest(GetRequest(2)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->StopRequest(GetRequest(0)); + tracker_->StopRequest(GetRequest(1)); + tracker_->StopRequest(GetRequest(2)); + EXPECT_NSEQ(nil, CheckActive()); + + TrimRequest(request_group_id_, GetURL(3)); + tracker_->StartRequest(GetRequest(3)); + EXPECT_NSEQ(nil, CheckActive()); + + tracker_->StopRequest(GetRequest(3)); + EXPECT_NSEQ(nil, CheckActive()); + + EndPage(request_group_id_, GetURL(3)); +} + +TEST_F(RequestTrackerTest, CaptureHeaders) { + std::string headers = + "HTTP/1.1 200 OK\n" + "content-type: multipart/mixed; boundary=inner\n" + "content-disposition: attachment; filename=\"name.pdf\"\n" + "X-Auto-Login: Hello World\n\n"; + for (size_t i = 0; i < headers.length(); i++) { + if (headers.data()[i] == '\n') + const_cast<char*>(headers.data())[i] = '\0'; + } + net::URLRequest* request = GetRequest(0); + const_cast<net::HttpResponseInfo&>(request->response_info()).headers = + new net::HttpResponseHeaders(headers); + // |job| will be owned by |request| and released from its destructor. + net::URLRequestTestJob* job = new net::URLRequestTestJob( + request, request->context()->network_delegate(), headers, "", false); + AddInterceptorToRequest(0)->set_main_intercept_job(job); + request->Start(); + + tracker_->StartRequest(request); + tracker_->CaptureHeaders(request); + tracker_->StopRequest(request); + loop_.RunUntilIdle(); + EXPECT_TRUE([receiver_ headers]->HasHeaderValue("X-Auto-Login", + "Hello World")); + std::string mimeType; + EXPECT_TRUE([receiver_ headers]->GetMimeType(&mimeType)); + EXPECT_EQ("multipart/mixed", mimeType); + EXPECT_TRUE([receiver_ headers]->HasHeaderValue( + "Content-Disposition", "attachment; filename=\"name.pdf\"")); +} + +// Do-nothing mock CertificatePolicyCache. Allows all certs for all hosts. +class MockCertificatePolicyCache : public web::CertificatePolicyCache { + public: + MockCertificatePolicyCache() {} + + void AllowCertForHost(net::X509Certificate* cert, + const std::string& host, + net::CertStatus error) override { + } + + web::CertPolicy::Judgment QueryPolicy(net::X509Certificate* cert, + const std::string& host, + net::CertStatus error) override { + return web::CertPolicy::Judgment::ALLOWED; + } + + void ClearCertificatePolicies() override { + } + + private: + ~MockCertificatePolicyCache() override {} +}; + +void TwoStartsSSLCallback(bool* called, bool ok) { + *called = true; +} + +// crbug/386180 +TEST_F(RequestTrackerTest, DISABLED_TwoStartsNoEstimate) { + net::X509Certificate* cert = + new net::X509Certificate("subject", "issuer", base::Time::Now(), + base::Time::Max()); + net::SSLInfo ssl_info; + ssl_info.cert = cert; + ssl_info.cert_status = net::CERT_STATUS_AUTHORITY_INVALID; + scoped_refptr<MockCertificatePolicyCache> cache; + tracker_->SetCertificatePolicyCacheForTest(cache.get()); + TrimRequest(request_group_id_, GetSecureURL(0)); + tracker_->StartRequest(GetSecureRequest(0)); + tracker_->StartRequest(GetSecureRequest(1)); + bool request_0_called = false; + bool request_1_called = false; + tracker_->OnSSLCertificateError(GetSecureRequest(0), ssl_info, true, + base::Bind(&TwoStartsSSLCallback, + &request_0_called)); + tracker_->OnSSLCertificateError(GetSecureRequest(1), ssl_info, true, + base::Bind(&TwoStartsSSLCallback, + &request_1_called)); + EXPECT_TRUE(request_0_called); + EXPECT_TRUE(request_1_called); +} + +} // namespace diff --git a/ios/web/net/web_http_protocol_handler_delegate.h b/ios/web/net/web_http_protocol_handler_delegate.h new file mode 100644 index 0000000..e885134 --- /dev/null +++ b/ios/web/net/web_http_protocol_handler_delegate.h @@ -0,0 +1,30 @@ +// Copyright 2014 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. + +#ifndef IOS_WEB_NET_WEB_HTTP_PROTOCOL_HANDLER_DELEGATE_H_ +#define IOS_WEB_NET_WEB_HTTP_PROTOCOL_HANDLER_DELEGATE_H_ + +#include "base/memory/ref_counted.h" +#import "ios/net/crn_http_protocol_handler.h" + +namespace web { + +// Web-specific implementation of net::HTTPProtocolHandlerDelegate. +class WebHTTPProtocolHandlerDelegate : public net::HTTPProtocolHandlerDelegate { + public: + WebHTTPProtocolHandlerDelegate(net::URLRequestContextGetter* default_getter); + ~WebHTTPProtocolHandlerDelegate() override; + + // net::HTTPProtocolHandlerDelegate implementation: + bool CanHandleRequest(NSURLRequest* request) override; + bool IsRequestSupported(NSURLRequest* request) override; + net::URLRequestContextGetter* GetDefaultURLRequestContext() override; + + private: + scoped_refptr<net::URLRequestContextGetter> default_getter_; +}; + +} // namespace web + +#endif // IOS_WEB_NET_WEB_HTTP_PROTOCOL_HANDLER_DELEGATE_H_ diff --git a/ios/web/net/web_http_protocol_handler_delegate.mm b/ios/web/net/web_http_protocol_handler_delegate.mm new file mode 100644 index 0000000..9405fbe --- /dev/null +++ b/ios/web/net/web_http_protocol_handler_delegate.mm @@ -0,0 +1,58 @@ +// Copyright 2014 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 "ios/web/net/web_http_protocol_handler_delegate.h" + +#include "ios/web/public/url_scheme_util.h" +#include "ios/web/public/web_client.h" +#import "ios/web/web_state/ui/crw_static_file_web_view.h" +#include "net/url_request/url_request_context_getter.h" +#include "url/gurl.h" + +namespace { + +bool IsAppSpecificScheme(NSURL* url) { + NSString* scheme = [url scheme]; + if (![scheme length]) + return false; + // Use the GURL implementation, but with a scheme-only URL to avoid + // unnecessary parsing in GURL construction. + GURL gurl([[scheme stringByAppendingString:@":"] UTF8String]); + return web::GetWebClient()->IsAppSpecificURL(gurl); +} + +} // namespace + +namespace web { + +WebHTTPProtocolHandlerDelegate::WebHTTPProtocolHandlerDelegate( + net::URLRequestContextGetter* default_getter) + : default_getter_(default_getter) { + DCHECK(default_getter_); +} + +WebHTTPProtocolHandlerDelegate::~WebHTTPProtocolHandlerDelegate() { +} + +bool WebHTTPProtocolHandlerDelegate::CanHandleRequest(NSURLRequest* request) { + // Accept all the requests. If we declined a request, it would then be passed + // to the default iOS network stack, which would possibly load it. + // As we want to control what is loaded, we have to prevent the default stack + // from loading anything. + return true; +} + +bool WebHTTPProtocolHandlerDelegate::IsRequestSupported(NSURLRequest* request) { + return web::UrlHasWebScheme([request URL]) || + [CRWStaticFileWebView isStaticFileRequest:request] || + (IsAppSpecificScheme([request URL]) && + IsAppSpecificScheme([request mainDocumentURL])); +} + +net::URLRequestContextGetter* +WebHTTPProtocolHandlerDelegate::GetDefaultURLRequestContext() { + return default_getter_.get(); +} + +} // namespace web diff --git a/ios/web/net/web_http_protocol_handler_delegate_unittest.mm b/ios/web/net/web_http_protocol_handler_delegate_unittest.mm new file mode 100644 index 0000000..5045374 --- /dev/null +++ b/ios/web/net/web_http_protocol_handler_delegate_unittest.mm @@ -0,0 +1,128 @@ +// Copyright 2014 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 "ios/web/net/web_http_protocol_handler_delegate.h" + +#import <Foundation/Foundation.h> + +#include "base/mac/scoped_nsobject.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/thread_task_runner_handle.h" +#include "ios/web/public/web_client.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace web { + +namespace { + +// Test application specific scheme. +const char kAppSpecificScheme[] = "appspecific"; + +// URLs expected to be supported. +const char* kSupportedURLs[] = { + "http://foo.com", + "https://foo.com", + "data:text/html;charset=utf-8,Hello", +}; + +// URLs expected to be unsupported. +const char* kUnsupportedURLs[] = { + "foo:blank", // Unknown scheme. + "appspecific:blank", // No main document URL. +}; + +// Test web client with an application specific scheme. +class AppSpecificURLTestWebClient : public WebClient { + public: + bool IsAppSpecificURL(const GURL& url) const override { + return url.SchemeIs(kAppSpecificScheme); + } +}; + +class WebHTTPProtocolHandlerDelegateTest : public testing::Test { + public: + WebHTTPProtocolHandlerDelegateTest() + : context_getter_(new net::TestURLRequestContextGetter( + base::ThreadTaskRunnerHandle::Get())), + delegate_(new WebHTTPProtocolHandlerDelegate(context_getter_.get())), + original_web_client_(GetWebClient()), + web_client_(new AppSpecificURLTestWebClient) { + SetWebClient(web_client_.get()); + } + + ~WebHTTPProtocolHandlerDelegateTest() override { + SetWebClient(original_web_client_); + } + + protected: + base::MessageLoop message_loop_; + scoped_refptr<net::URLRequestContextGetter> context_getter_; + scoped_ptr<WebHTTPProtocolHandlerDelegate> delegate_; + WebClient* original_web_client_; // Stash the web client, Weak. + scoped_ptr<AppSpecificURLTestWebClient> web_client_; +}; + +} // namespace + +TEST_F(WebHTTPProtocolHandlerDelegateTest, IsRequestSupported) { + base::scoped_nsobject<NSMutableURLRequest> request; + + for (unsigned int i = 0; i < arraysize(kSupportedURLs); ++i) { + base::scoped_nsobject<NSString> url_string( + [[NSString alloc] initWithUTF8String:kSupportedURLs[i]]); + request.reset([[NSMutableURLRequest alloc] + initWithURL:[NSURL URLWithString:url_string]]); + EXPECT_TRUE(delegate_->IsRequestSupported(request)) + << kSupportedURLs[i] << " should be supported."; + } + + for (unsigned int i = 0; i < arraysize(kUnsupportedURLs); ++i) { + base::scoped_nsobject<NSString> url_string( + [[NSString alloc] initWithUTF8String:kUnsupportedURLs[i]]); + request.reset([[NSMutableURLRequest alloc] + initWithURL:[NSURL URLWithString:url_string]]); + EXPECT_FALSE(delegate_->IsRequestSupported(request)) + << kUnsupportedURLs[i] << " should NOT be supported."; + } + + // Application specific scheme with main document URL. + request.reset([[NSMutableURLRequest alloc] + initWithURL:[NSURL URLWithString:@"appspecific:blank"]]); + [request setMainDocumentURL:[NSURL URLWithString:@"http://foo"]]; + EXPECT_FALSE(delegate_->IsRequestSupported(request)); + [request setMainDocumentURL:[NSURL URLWithString:@"appspecific:main"]]; + EXPECT_TRUE(delegate_->IsRequestSupported(request)); + request.reset([[NSMutableURLRequest alloc] + initWithURL:[NSURL URLWithString:@"foo:blank"]]); + [request setMainDocumentURL:[NSURL URLWithString:@"appspecific:main"]]; + EXPECT_FALSE(delegate_->IsRequestSupported(request)); +} + +TEST_F(WebHTTPProtocolHandlerDelegateTest, IsRequestSupportedMalformed) { + base::scoped_nsobject<NSURLRequest> request; + + // Null URL. + request.reset([[NSMutableURLRequest alloc] init]); + ASSERT_FALSE([request URL]); + EXPECT_FALSE(delegate_->IsRequestSupported(request)); + + // URL with no scheme. + request.reset( + [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"foo"]]); + ASSERT_TRUE([request URL]); + ASSERT_FALSE([[request URL] scheme]); + EXPECT_FALSE(delegate_->IsRequestSupported(request)); + + // Empty scheme. + request.reset( + [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@":foo"]]); + ASSERT_TRUE([request URL]); + ASSERT_TRUE([[request URL] scheme]); + ASSERT_FALSE([[[request URL] scheme] length]); + EXPECT_FALSE(delegate_->IsRequestSupported(request)); +} + +} // namespace web diff --git a/ios/web/public/browser_state.h b/ios/web/public/browser_state.h index b4f2ade..44facdd 100644 --- a/ios/web/public/browser_state.h +++ b/ios/web/public/browser_state.h @@ -16,6 +16,7 @@ class URLRequestContextGetter; } namespace web { +class CertificatePolicyCache; // This class holds the context needed for a browsing session. // It lives on the UI thread. All these methods must only be called on the UI @@ -24,6 +25,10 @@ class BrowserState : public base::SupportsUserData { public: ~BrowserState() override; + // static + static scoped_refptr<CertificatePolicyCache> GetCertificatePolicyCache( + BrowserState* browser_state); + // Returns whether this BrowserState is incognito. Default is false. virtual bool IsOffTheRecord() const = 0; diff --git a/ios/web/public/cert_store.h b/ios/web/public/cert_store.h new file mode 100644 index 0000000..b6b0855 --- /dev/null +++ b/ios/web/public/cert_store.h @@ -0,0 +1,49 @@ +// Copyright 2015 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. + +#ifndef IOS_WEB_PUBLIC_BROWSER_CERT_STORE_H_ +#define IOS_WEB_PUBLIC_BROWSER_CERT_STORE_H_ + +#include "base/memory/ref_counted.h" + +namespace net { +class X509Certificate; +} + +namespace web { + +// The purpose of the cert store is to provide an easy way to store/retrieve +// X509Certificate objects. When stored, an X509Certificate object is +// associated with a group, which can be cleared when it is no longer useful. +// It can be accessed from the UI and IO threads (it is thread-safe). +// Note that the cert ids will overflow if more than 2^32 - 1 certs are +// registered in one browsing session (which is highly unlikely to happen). +class CertStore { + public: + // Returns the singleton instance of the CertStore. + static CertStore* GetInstance(); + + // Stores the specified cert and returns the id associated with it. The cert + // is associated to the specified RequestTracker. + // When all the RequestTrackers associated with a cert have been closed, the + // cert is removed from the store. + // Note: ids starts at 1. + virtual int StoreCert(net::X509Certificate* cert, int group_id) = 0; + + // Tries to retrieve the previously stored cert associated with the specified + // |cert_id|. Returns whether the cert could be found, and, if |cert| is + // non-nullptr, copies it in. + virtual bool RetrieveCert(int cert_id, + scoped_refptr<net::X509Certificate>* cert) = 0; + + // Removes all the certs associated with the specified groups from the store. + virtual void RemoveCertsForGroup(int group_id) = 0; + + protected: + virtual ~CertStore() {} +}; + +} // namespace web + +#endif // IOS_WEB_PUBLIC_BROWSER_CERT_STORE_H_ diff --git a/ios/web/public/test/test_web_client.h b/ios/web/public/test/test_web_client.h new file mode 100644 index 0000000..b6eb167 --- /dev/null +++ b/ios/web/public/test/test_web_client.h @@ -0,0 +1,45 @@ +// Copyright 2014 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. + +#ifndef WEB_TEST_WEB_PUBLIC_TEST_TEST_WEB_CLIENT_H_ +#define WEB_TEST_WEB_PUBLIC_TEST_TEST_WEB_CLIENT_H_ + +#import <Foundation/Foundation.h> + +#include "base/compiler_specific.h" +#include "base/mac/scoped_nsobject.h" +#include "base/memory/scoped_ptr.h" +#include "ios/web/public/web_client.h" + +namespace web { + +class WebViewFactory; + +// A WebClient used for testing purposes. +class TestWebClient : public web::WebClient { + public: + TestWebClient(); + ~TestWebClient() override; + // WebClient implementation. + std::string GetUserAgent(bool is_desktop_user_agent) const override; + NSString* GetEarlyPageScript(web::WebViewType web_view_type) const override; + + // Changes the user agent for testing purposes. Passing true + // for |is_desktop_user_agent| affects the result of GetUserAgent(true) call. + // Passing false affects the result of GetUserAgent(false). + void SetUserAgent(const std::string& user_agent, bool is_desktop_user_agent); + + // Changes Early Page Script for testing purposes. + void SetEarlyPageScript(NSString* page_script, + web::WebViewType web_view_type); + + private: + std::string user_agent_; + std::string desktop_user_agent_; + base::scoped_nsobject<NSMutableDictionary> early_page_scripts_; +}; + +} // namespace web + +#endif // WEB_TEST_WEB_PUBLIC_TEST_TEST_WEB_CLIENT_H_ diff --git a/ios/web/public/test/test_web_client.mm b/ios/web/public/test/test_web_client.mm new file mode 100644 index 0000000..d4b1e5c --- /dev/null +++ b/ios/web/public/test/test_web_client.mm @@ -0,0 +1,41 @@ +// Copyright 2014 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. + +#import "ios/web/public/test/test_web_client.h" + +#include "base/strings/sys_string_conversions.h" + +namespace web { + +TestWebClient::TestWebClient() + : early_page_scripts_([[NSMutableDictionary alloc] init]) { +} + +TestWebClient::~TestWebClient() { +} + +std::string TestWebClient::GetUserAgent(bool desktop_user_agent) const { + return desktop_user_agent ? desktop_user_agent_ : user_agent_; +} + +void TestWebClient::SetUserAgent(const std::string& user_agent, + bool is_desktop_user_agent) { + if (is_desktop_user_agent) + desktop_user_agent_ = user_agent; + else + user_agent_ = user_agent; +} + +NSString* TestWebClient::GetEarlyPageScript( + web::WebViewType web_view_type) const { + NSString* result = [early_page_scripts_ objectForKey:@(web_view_type)]; + return result ? result : @""; +} + +void TestWebClient::SetEarlyPageScript(NSString* page_script, + web::WebViewType web_view_type) { + [early_page_scripts_ setObject:page_script forKey:@(web_view_type)]; +} + +} // namespace web diff --git a/ios/web/ui_web_view_util.h b/ios/web/ui_web_view_util.h new file mode 100644 index 0000000..46b36ab --- /dev/null +++ b/ios/web/ui_web_view_util.h @@ -0,0 +1,24 @@ +// Copyright 2014 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. + +#ifndef IOS_WEB_UI_WEB_VIEW_UTIL_H_ +#define IOS_WEB_UI_WEB_VIEW_UTIL_H_ + +#include <Foundation/Foundation.h> + +namespace web { + +// Registers the user agent encoding |request_group_id| in the user defaults. +// This is a utility method, used to workaround crbug.com/228397. Do not use +// it for other purposes. +void BuildAndRegisterUserAgentForUIWebView(NSString* request_group_id, + BOOL use_desktop_user_agent); + +// Registers |user_agent| as the user agent string to be used by the UIWebView +// instances that are created from now on. +void RegisterUserAgentForUIWebView(NSString* user_agent); + +} // namespace web + +#endif // IOS_WEB_UI_WEB_VIEW_UTIL_H_ diff --git a/ios/web/ui_web_view_util.mm b/ios/web/ui_web_view_util.mm new file mode 100644 index 0000000..35c6347 --- /dev/null +++ b/ios/web/ui_web_view_util.mm @@ -0,0 +1,40 @@ +// Copyright 2014 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. + +#import "ios/web/ui_web_view_util.h" + +#include "base/logging.h" +#include "base/mac/scoped_nsobject.h" +#include "base/strings/sys_string_conversions.h" +#include "ios/web/net/request_group_util.h" +#include "ios/web/public/web_client.h" + +namespace { +// Returns true if given Request Group ID string contains only decimal digits. +BOOL IsValidRequestGroupID(NSString* request_group_id) { + return [[NSCharacterSet decimalDigitCharacterSet] isSupersetOfSet: + [NSCharacterSet characterSetWithCharactersInString:request_group_id]]; +} +} // namespace + +namespace web { + +void BuildAndRegisterUserAgentForUIWebView(NSString* request_group_id, + BOOL use_desktop_user_agent) { + DCHECK(!request_group_id || IsValidRequestGroupID(request_group_id)); + DCHECK(web::GetWebClient()); + std::string baseUserAgent = web::GetWebClient()->GetUserAgent( + use_desktop_user_agent); + web::RegisterUserAgentForUIWebView( + web::AddRequestGroupIDToUserAgent(base::SysUTF8ToNSString(baseUserAgent), + request_group_id)); +} + +void RegisterUserAgentForUIWebView(NSString* user_agent) { + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"UserAgent" : user_agent + }]; +} + +} // namespace web diff --git a/ios/web/ui_web_view_util_unittest.mm b/ios/web/ui_web_view_util_unittest.mm new file mode 100644 index 0000000..0354579 --- /dev/null +++ b/ios/web/ui_web_view_util_unittest.mm @@ -0,0 +1,54 @@ +// Copyright 2014 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. + +#import "ios/web/ui_web_view_util.h" + +#import "ios/web/public/test/test_web_client.h" +#include "testing/gtest_mac.h" +#include "testing/platform_test.h" + +namespace { + +// Returns user agent string registered for UIWebView. +NSString* GetUserAgent() { + return [[NSUserDefaults standardUserDefaults] stringForKey:@"UserAgent"]; +} + +class UIWebViewUtilTest : public PlatformTest { + protected: + void SetUp() override { + PlatformTest::SetUp(); + test_web_client_.SetUserAgent("DesktopUA", true); + test_web_client_.SetUserAgent("RegularUA", false); + web::SetWebClient(&test_web_client_); + } + void TearDown() override { + web::SetWebClient(nullptr); + PlatformTest::TearDown(); + } + private: + // WebClient that returns test user agent. + web::TestWebClient test_web_client_; +}; + +// Tests registration of a non-desktop user agent. +TEST_F(UIWebViewUtilTest, BuildAndRegisterNonDesktopUserAgentForUIWebView) { + web::BuildAndRegisterUserAgentForUIWebView(@"1231546541321", NO); + EXPECT_NSEQ(@"RegularUA (1231546541321)", GetUserAgent()); +} + +// Tests registration of a desktop user agent. +TEST_F(UIWebViewUtilTest, BuildAndRegisterDesktopUserAgentForUIWebView) { + web::BuildAndRegisterUserAgentForUIWebView(@"1231546541321", YES); + EXPECT_NSEQ(@"DesktopUA (1231546541321)", GetUserAgent()); +} + +// Tests web::RegisterUserAgentForUIWebView function that it correctly registers +// arbitrary user agent. +TEST_F(UIWebViewUtilTest, RegisterUserAgentForUIWebView) { + web::RegisterUserAgentForUIWebView(@"UA"); + EXPECT_NSEQ(@"UA", GetUserAgent()); +} + +} // namespace diff --git a/ios/web/web_state/ui/crw_static_file_web_view.h b/ios/web/web_state/ui/crw_static_file_web_view.h new file mode 100644 index 0000000..1b7e54d --- /dev/null +++ b/ios/web/web_state/ui/crw_static_file_web_view.h @@ -0,0 +1,36 @@ +// Copyright 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. + +#ifndef IOS_WEB_WEB_STATE_UI_CRW_STATIC_FILE_WEB_VIEW_H_ +#define IOS_WEB_WEB_STATE_UI_CRW_STATIC_FILE_WEB_VIEW_H_ + +#import <UIKit/UIKit.h> + +#import "ios/web/net/crw_request_tracker_delegate.h" + +namespace web { +class BrowserState; +} + +// A subclass of UIWebView that specializes in presenting static html file +// content. Use this type of UIWebView that needs to display static HTML +// content from application bundle. +// TODO(shreyasv): Remove this class, since nobody is using it. +@interface CRWStaticFileWebView : UIWebView<CRWRequestTrackerDelegate> + +// Creates a new UIWebView associated with the given |browserState|. +// |browserState| may be nullptr. +- (instancetype)initWithFrame:(CGRect)frame + browserState:(web::BrowserState*)browserState; + +// Returns whether the request should be allowed for rendering into a +// special UIWebView that allows static file content. +// TODO(marq): Since this is only used to inject a FileRequestVerfier into +// HttpProtocolHandler, instead make this a method that returns a block of +// that type. ++ (BOOL)isStaticFileRequest:(NSURLRequest*)request; + +@end + +#endif // IOS_WEB_WEB_STATE_UI_CRW_STATIC_FILE_WEB_VIEW_H_ diff --git a/ios/web/web_state/ui/crw_static_file_web_view.mm b/ios/web/web_state/ui/crw_static_file_web_view.mm new file mode 100644 index 0000000..e40a0af --- /dev/null +++ b/ios/web/web_state/ui/crw_static_file_web_view.mm @@ -0,0 +1,149 @@ +// Copyright 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. + +#import "ios/web/web_state/ui/crw_static_file_web_view.h" + +#include "base/logging.h" +#include "ios/web/net/request_group_util.h" +#include "ios/web/net/request_tracker_impl.h" +#include "ios/web/public/browser_state.h" +#import "ios/web/ui_web_view_util.h" +#include "net/http/http_response_headers.h" + +namespace { +NSString* const kStaticFileUserAgent = @"UIWebViewForStaticFileContent"; +} + +@interface CRWStaticFileWebView () { + // Saves the user agent in order to set it on future requests. + base::scoped_nsobject<NSString> userAgent_; + // Saves the identification assigned to this web view for request + // tracking. This identifiation is required for terminating request + // tracking. + scoped_refptr<web::RequestTrackerImpl> requestTracker_; +} + +// Whether the User-Agent is the allowed user agent. ++ (BOOL)isStaticFileUserAgent:(NSString*)userAgent; +@end + +@implementation CRWStaticFileWebView + +- (instancetype)initWithFrame:(CGRect)frame + browserState:(web::BrowserState*)browserState { + // Register the user agent before instantiating the UIWebView. + NSString* requestGroupID = nil; + NSString* userAgent = kStaticFileUserAgent; + if (browserState) { + requestGroupID = web::GenerateNewRequestGroupID(); + userAgent = web::AddRequestGroupIDToUserAgent(userAgent, requestGroupID); + } + web::RegisterUserAgentForUIWebView(userAgent); + self = [super initWithFrame:frame]; + if (self) { + DCHECK(!browserState || [requestGroupID length]); + if (browserState) { + userAgent_.reset([userAgent copy]); + // If |browserState| is not nullptr, associate this UIWebView with the + // given |requestGroupID| which cannot be nil or zero-length. + // RequestTracker keeps track of requests to this requestGroupID until + // this UIWebView is deallocated. UIWebViews not associated with a + // requestGroupID will issue requests in the global request context. + base::scoped_nsobject<NSString> requestGroupIDCopy([requestGroupID copy]); + requestTracker_ = web::RequestTrackerImpl::CreateTrackerForRequestGroupID( + requestGroupIDCopy, + browserState, + browserState->GetRequestContext(), + self); + } + } + return self; +} + +- (void)dealloc { + if (requestTracker_.get()) + requestTracker_->Close(); + [super dealloc]; +} + ++ (BOOL)isStaticFileRequest:(NSURLRequest*)request { + NSString* userAgent = [request allHTTPHeaderFields][@"User-Agent"]; + if (userAgent) { + return [CRWStaticFileWebView isStaticFileUserAgent:userAgent]; + } + + // If a request originated from another file:/// page, the User-Agent + // will not be there. To be safe, check that the request is for image + // resources only. + // TODO(pkl): This current test to allow nil User-Agent and images to + // be loaded. A more air-tight implementation should inline images as + // "data" instead. See crbug.com/228603 + NSString* suffix = [[request URL] pathExtension]; + return [@[ @"png", @"jpg", @"jpeg" ] containsObject:[suffix lowercaseString]]; +} + ++ (BOOL)isStaticFileUserAgent:(NSString*)userAgent { + return [userAgent hasPrefix:kStaticFileUserAgent]; +} + +#pragma mark - +#pragma mark UIWebView + +// On iOS 6.0, UIWebView does not set the user agent for static file requests. +// The solution consists of overriding |loadRequest:| and setting the user agent +// before loading the request. +- (void)loadRequest:(NSURLRequest*)request { + base::scoped_nsobject<NSMutableURLRequest> mutableRequest( + (NSMutableURLRequest*)[request mutableCopy]); + [mutableRequest setValue:userAgent_ forHTTPHeaderField:@"User-Agent"]; + [super loadRequest:mutableRequest]; +} + +#pragma mark - +#pragma mark CRWRequestTrackerDelegate + +- (BOOL)isForStaticFileRequests { + return YES; +} + +- (void)handleResponseHeaders:(net::HttpResponseHeaders*)headers + requestUrl:(const GURL&)requestUrl { + std::string mimeType; + DCHECK(headers->GetMimeType(&mimeType) && mimeType == "text/html"); + DCHECK(!headers->HasHeader("X-Auto-Login")); + DCHECK(!headers->HasHeader("content-disposition")); +} + +// The following delegate functions are not expected to be called since +// the page is intended to be a static HTML page. + +- (void)updatedSSLStatus:(const web::SSLStatus&)sslStatus + forPageUrl:(const GURL&)url + userInfo:(id)userInfo { + NOTREACHED(); +} + +- (void)presentSSLError:(const net::SSLInfo&)info + forSSLStatus:(const web::SSLStatus&)status + onUrl:(const GURL&)url + recoverable:(BOOL)recoverable + callback:(SSLErrorCallback)shouldContinue { + NOTREACHED(); +} + +- (void)updatedProgress:(float)progress { + NOTREACHED(); +} + +- (void)certificateUsed:(net::X509Certificate*)certificate + forHost:(const std::string&)host + status:(net::CertStatus)status { + NOTREACHED(); +} + +- (void)clearCertificates { + NOTREACHED(); +} + +@end diff --git a/ios/web/web_state/ui/crw_static_file_web_view_unittest.mm b/ios/web/web_state/ui/crw_static_file_web_view_unittest.mm new file mode 100644 index 0000000..bcf12eb --- /dev/null +++ b/ios/web/web_state/ui/crw_static_file_web_view_unittest.mm @@ -0,0 +1,89 @@ +// Copyright 2014 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. + +#import "ios/web/web_state/ui/crw_static_file_web_view.h" + +#import <Foundation/Foundation.h> + +#include "base/mac/scoped_nsobject.h" +#include "ios/web/net/request_group_util.h" +#include "ios/web/public/test/test_browser_state.h" +#include "ios/web/public/test/test_web_thread_bundle.h" +#import "ios/web/ui_web_view_util.h" +#include "testing/platform_test.h" +#import "third_party/ocmock/OCMock/OCMock.h" + +@interface CRWStaticFileWebView (Testing) ++ (BOOL)isStaticFileUserAgent:(NSString*)userAgent; +@end + +class CRWStaticFileWebViewTest : public PlatformTest { + public: + CRWStaticFileWebViewTest() {} + + protected: + void SetUp() override { PlatformTest::SetUp(); } + void TearDown() override { PlatformTest::TearDown(); } + + // Gets the user agent of |web_view|, using javascript. + NSString* GetWebViewUserAgent(UIWebView* web_view) { + NSString* const kJsUserAgent = @"navigator.userAgent"; + return [web_view stringByEvaluatingJavaScriptFromString:kJsUserAgent]; + }; + + web::TestWebThreadBundle thread_bundle_; + web::TestBrowserState browser_state_; +}; + +// Tests that requests for images are considered as static file requests, +// regardless of the user agent. +TEST_F(CRWStaticFileWebViewTest, TestIsStaticImageRequestTrue) { + // Empty dictionary so User-Agent check fails. + NSDictionary* dictionary = @{}; + NSURL* url = [NSURL URLWithString:@"file:///show/this.png"]; + id mockRequest = [OCMockObject mockForClass:[NSURLRequest class]]; + [[[mockRequest stub] andReturn:dictionary] allHTTPHeaderFields]; + [[[mockRequest stub] andReturn:url] URL]; + EXPECT_TRUE([CRWStaticFileWebView isStaticFileRequest:mockRequest]); +} + +// Tests that requests for files are considered as static file requests if they +// have the static file user agent. +TEST_F(CRWStaticFileWebViewTest, TestIsStaticFileRequestTrue) { + base::scoped_nsobject<UIWebView> webView( + [[CRWStaticFileWebView alloc] initWithFrame:CGRectZero + browserState:&browser_state_]); + EXPECT_TRUE(webView); + NSString* userAgent = GetWebViewUserAgent(webView); + NSDictionary* dictionary = @{ @"User-Agent" : userAgent }; + NSURL* url = [NSURL URLWithString:@"file:///some/random/url.html"]; + id mockRequest = [OCMockObject mockForClass:[NSURLRequest class]]; + [[[mockRequest stub] andReturn:dictionary] allHTTPHeaderFields]; + [[[mockRequest stub] andReturn:url] URL]; + EXPECT_TRUE([CRWStaticFileWebView isStaticFileRequest:mockRequest]); +} + + +// Tests that arbitrary files cannot be retrieved by a web view for +// static file content. +TEST_F(CRWStaticFileWebViewTest, TestIsStaticFileRequestFalse) { + // Empty dictionary so User-Agent check fails. + NSDictionary* dictionary = @{}; + NSURL* url = [NSURL URLWithString:@"file:///steal/this/file.html"]; + id mockRequest = [OCMockObject mockForClass:[NSURLRequest class]]; + [[[mockRequest stub] andReturn:dictionary] allHTTPHeaderFields]; + [[[mockRequest stub] andReturn:url] URL]; + EXPECT_FALSE([CRWStaticFileWebView isStaticFileRequest:mockRequest]); +} + +// Tests that the user agent of a CRWStaticFileWebView includes a request group +// ID. +TEST_F(CRWStaticFileWebViewTest, TestExtractRequestGroupIDStaticFile) { + base::scoped_nsobject<UIWebView> webView( + [[CRWStaticFileWebView alloc] initWithFrame:CGRectZero + browserState:&browser_state_]); + EXPECT_TRUE(webView); + NSString* userAgentString = GetWebViewUserAgent(webView); + EXPECT_TRUE(web::ExtractRequestGroupIDFromUserAgent(userAgentString)); +} |