Home | History | Annotate | Download | only in webui
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/ui/webui/devtools_ui.h"
      6 
      7 #include <string>
      8 
      9 #include "base/command_line.h"
     10 #include "base/memory/ref_counted_memory.h"
     11 #include "base/memory/scoped_ptr.h"
     12 #include "base/strings/string_util.h"
     13 #include "base/strings/stringprintf.h"
     14 #include "chrome/browser/devtools/device/devtools_android_bridge.h"
     15 #include "chrome/browser/devtools/devtools_target_impl.h"
     16 #include "chrome/browser/profiles/profile.h"
     17 #include "chrome/common/chrome_switches.h"
     18 #include "chrome/common/url_constants.h"
     19 #include "content/public/browser/browser_thread.h"
     20 #include "content/public/browser/devtools_http_handler.h"
     21 #include "content/public/browser/navigation_controller.h"
     22 #include "content/public/browser/navigation_details.h"
     23 #include "content/public/browser/navigation_entry.h"
     24 #include "content/public/browser/url_data_source.h"
     25 #include "content/public/browser/web_contents.h"
     26 #include "content/public/browser/web_ui.h"
     27 #include "net/url_request/url_fetcher.h"
     28 #include "net/url_request/url_fetcher_delegate.h"
     29 #include "net/url_request/url_request_context_getter.h"
     30 #include "ui/base/resource/resource_bundle.h"
     31 
     32 using content::BrowserThread;
     33 using content::WebContents;
     34 
     35 namespace {
     36 
     37 std::string PathWithoutParams(const std::string& path) {
     38   return GURL(std::string("chrome-devtools://devtools/") + path)
     39       .path().substr(1);
     40 }
     41 
     42 const char kRemoteFrontendDomain[] = "chrome-devtools-frontend.appspot.com";
     43 const char kRemoteFrontendBase[] =
     44     "https://chrome-devtools-frontend.appspot.com/";
     45 const char kHttpNotFound[] = "HTTP/1.1 404 Not Found\n\n";
     46 
     47 #if defined(DEBUG_DEVTOOLS)
     48 // Local frontend url provided by InspectUI.
     49 const char kFallbackFrontendURL[] =
     50     "chrome-devtools://devtools/bundled/devtools.html";
     51 #else
     52 // URL causing the DevTools window to display a plain text warning.
     53 const char kFallbackFrontendURL[] =
     54     "data:text/plain,Cannot load DevTools frontend from an untrusted origin";
     55 #endif  // defined(DEBUG_DEVTOOLS)
     56 
     57 const char kRemoteOpenPrefix[] = "remote/open";
     58 
     59 #if defined(DEBUG_DEVTOOLS)
     60 const char kLocalSerial[] = "local";
     61 #endif  // defined(DEBUG_DEVTOOLS)
     62 
     63 // FetchRequest ---------------------------------------------------------------
     64 
     65 class FetchRequest : public net::URLFetcherDelegate {
     66  public:
     67   FetchRequest(net::URLRequestContextGetter* request_context,
     68                const GURL& url,
     69                const content::URLDataSource::GotDataCallback& callback);
     70 
     71  private:
     72   virtual ~FetchRequest() {}
     73   virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
     74   scoped_ptr<net::URLFetcher> fetcher_;
     75   content::URLDataSource::GotDataCallback callback_;
     76 };
     77 
     78 FetchRequest::FetchRequest(
     79     net::URLRequestContextGetter* request_context,
     80     const GURL& url,
     81     const content::URLDataSource::GotDataCallback& callback)
     82     : callback_(callback) {
     83   if (!url.is_valid()) {
     84     OnURLFetchComplete(NULL);
     85     return;
     86   }
     87 
     88   fetcher_.reset(net::URLFetcher::Create(url, net::URLFetcher::GET, this));
     89   fetcher_->SetRequestContext(request_context);
     90   fetcher_->Start();
     91 }
     92 
     93 void FetchRequest::OnURLFetchComplete(const net::URLFetcher* source) {
     94   std::string response;
     95   if (source)
     96     source->GetResponseAsString(&response);
     97   else
     98     response = kHttpNotFound;
     99 
    100   callback_.Run(base::RefCountedString::TakeString(&response));
    101   delete this;
    102 }
    103 
    104 // DevToolsDataSource ---------------------------------------------------------
    105 
    106 std::string GetMimeTypeForPath(const std::string& path) {
    107   std::string filename = PathWithoutParams(path);
    108   if (EndsWith(filename, ".html", false)) {
    109     return "text/html";
    110   } else if (EndsWith(filename, ".css", false)) {
    111     return "text/css";
    112   } else if (EndsWith(filename, ".js", false)) {
    113     return "application/javascript";
    114   } else if (EndsWith(filename, ".png", false)) {
    115     return "image/png";
    116   } else if (EndsWith(filename, ".gif", false)) {
    117     return "image/gif";
    118   } else if (EndsWith(filename, ".manifest", false)) {
    119     return "text/cache-manifest";
    120   }
    121   return "text/html";
    122 }
    123 
    124 // An URLDataSource implementation that handles chrome-devtools://devtools/
    125 // requests. Three types of requests could be handled based on the URL path:
    126 // 1. /bundled/: bundled DevTools frontend is served.
    127 // 2. /remote/: remote DevTools frontend is served from App Engine.
    128 // 3. /remote/open/: query is URL which is opened on remote device.
    129 class DevToolsDataSource : public content::URLDataSource {
    130  public:
    131   explicit DevToolsDataSource(net::URLRequestContextGetter* request_context);
    132 
    133   // content::URLDataSource implementation.
    134   virtual std::string GetSource() const OVERRIDE;
    135 
    136   virtual void StartDataRequest(
    137       const std::string& path,
    138       int render_process_id,
    139       int render_frame_id,
    140       const content::URLDataSource::GotDataCallback& callback) OVERRIDE;
    141 
    142  private:
    143   // content::URLDataSource overrides.
    144   virtual std::string GetMimeType(const std::string& path) const OVERRIDE;
    145   virtual bool ShouldAddContentSecurityPolicy() const OVERRIDE;
    146   virtual bool ShouldServeMimeTypeAsContentTypeHeader() const OVERRIDE;
    147 
    148   // Serves bundled DevTools frontend from ResourceBundle.
    149   void StartBundledDataRequest(
    150       const std::string& path,
    151       int render_process_id,
    152       int render_frame_id,
    153       const content::URLDataSource::GotDataCallback& callback);
    154 
    155   // Serves remote DevTools frontend from hard-coded App Engine domain.
    156   void StartRemoteDataRequest(
    157       const std::string& path,
    158       int render_process_id,
    159       int render_frame_id,
    160       const content::URLDataSource::GotDataCallback& callback);
    161 
    162   virtual ~DevToolsDataSource() {}
    163   scoped_refptr<net::URLRequestContextGetter> request_context_;
    164 
    165   DISALLOW_COPY_AND_ASSIGN(DevToolsDataSource);
    166 };
    167 
    168 DevToolsDataSource::DevToolsDataSource(
    169     net::URLRequestContextGetter* request_context)
    170     : request_context_(request_context) {
    171 }
    172 
    173 std::string DevToolsDataSource::GetSource() const {
    174   return chrome::kChromeUIDevToolsHost;
    175 }
    176 
    177 void DevToolsDataSource::StartDataRequest(
    178     const std::string& path,
    179     int render_process_id,
    180     int render_frame_id,
    181     const content::URLDataSource::GotDataCallback& callback) {
    182   // Serve request from local bundle.
    183   std::string bundled_path_prefix(chrome::kChromeUIDevToolsBundledPath);
    184   bundled_path_prefix += "/";
    185   if (StartsWithASCII(path, bundled_path_prefix, false)) {
    186     StartBundledDataRequest(path.substr(bundled_path_prefix.length()),
    187                             render_process_id, render_frame_id, callback);
    188     return;
    189   }
    190 
    191   // Serve static response while connecting to the remote device.
    192   if (StartsWithASCII(path, kRemoteOpenPrefix, false)) {
    193     if (!CommandLine::ForCurrentProcess()->HasSwitch(
    194             switches::kEnableDevToolsExperiments)) {
    195       callback.Run(NULL);
    196       return;
    197     }
    198     std::string response = "Connecting to the device...";
    199     callback.Run(base::RefCountedString::TakeString(&response));
    200     return;
    201   }
    202 
    203   // Serve request from remote location.
    204   std::string remote_path_prefix(chrome::kChromeUIDevToolsRemotePath);
    205   remote_path_prefix += "/";
    206   if (StartsWithASCII(path, remote_path_prefix, false)) {
    207     StartRemoteDataRequest(path.substr(remote_path_prefix.length()),
    208                            render_process_id, render_frame_id, callback);
    209     return;
    210   }
    211 
    212   callback.Run(NULL);
    213 }
    214 
    215 std::string DevToolsDataSource::GetMimeType(const std::string& path) const {
    216   return GetMimeTypeForPath(path);
    217 }
    218 
    219 bool DevToolsDataSource::ShouldAddContentSecurityPolicy() const {
    220   return false;
    221 }
    222 
    223 bool DevToolsDataSource::ShouldServeMimeTypeAsContentTypeHeader() const {
    224   return true;
    225 }
    226 
    227 void DevToolsDataSource::StartBundledDataRequest(
    228     const std::string& path,
    229     int render_process_id,
    230     int render_frame_id,
    231     const content::URLDataSource::GotDataCallback& callback) {
    232   std::string filename = PathWithoutParams(path);
    233 
    234   int resource_id =
    235       content::DevToolsHttpHandler::GetFrontendResourceId(filename);
    236 
    237   DLOG_IF(WARNING, -1 == resource_id) << "Unable to find dev tool resource: "
    238       << filename << ". If you compiled with debug_devtools=1, try running"
    239       " with --debug-devtools.";
    240   const ResourceBundle& rb = ResourceBundle::GetSharedInstance();
    241   scoped_refptr<base::RefCountedStaticMemory> bytes(rb.LoadDataResourceBytes(
    242       resource_id));
    243   callback.Run(bytes.get());
    244 }
    245 
    246 void DevToolsDataSource::StartRemoteDataRequest(
    247     const std::string& path,
    248     int render_process_id,
    249     int render_frame_id,
    250     const content::URLDataSource::GotDataCallback& callback) {
    251   GURL url = GURL(kRemoteFrontendBase + path);
    252   CHECK_EQ(url.host(), kRemoteFrontendDomain);
    253   new FetchRequest(request_context_.get(), url, callback);
    254 }
    255 
    256 // OpenRemotePageRequest ------------------------------------------------------
    257 
    258 class OpenRemotePageRequest : public DevToolsAndroidBridge::DeviceListListener {
    259  public:
    260   OpenRemotePageRequest(
    261       Profile* profile,
    262       const std::string url,
    263       const DevToolsAndroidBridge::RemotePageCallback& callback);
    264   virtual ~OpenRemotePageRequest() {}
    265 
    266  private:
    267   // DevToolsAndroidBridge::Listener overrides.
    268   virtual void DeviceListChanged(
    269       const DevToolsAndroidBridge::RemoteDevices& devices) OVERRIDE;
    270 
    271   bool OpenInBrowser(DevToolsAndroidBridge::RemoteBrowser* browser);
    272   void RemotePageOpened(DevToolsAndroidBridge::RemotePage* page);
    273 
    274   std::string url_;
    275   DevToolsAndroidBridge::RemotePageCallback callback_;
    276   bool opening_;
    277   scoped_refptr<DevToolsAndroidBridge> android_bridge_;
    278 
    279   DISALLOW_COPY_AND_ASSIGN(OpenRemotePageRequest);
    280 };
    281 
    282 OpenRemotePageRequest::OpenRemotePageRequest(
    283     Profile* profile,
    284     const std::string url,
    285     const DevToolsAndroidBridge::RemotePageCallback& callback)
    286     : url_(url),
    287       callback_(callback),
    288       opening_(false),
    289       android_bridge_(
    290           DevToolsAndroidBridge::Factory::GetForProfile(profile)) {
    291   android_bridge_->AddDeviceListListener(this);
    292 }
    293 
    294 void OpenRemotePageRequest::DeviceListChanged(
    295     const DevToolsAndroidBridge::RemoteDevices& devices) {
    296   if (opening_)
    297     return;
    298 
    299   for (DevToolsAndroidBridge::RemoteDevices::const_iterator dit =
    300       devices.begin(); dit != devices.end(); ++dit) {
    301     DevToolsAndroidBridge::RemoteDevice* device = dit->get();
    302     if (!device->is_connected())
    303       continue;
    304 
    305     DevToolsAndroidBridge::RemoteBrowsers& browsers = device->browsers();
    306     for (DevToolsAndroidBridge::RemoteBrowsers::iterator bit =
    307         browsers.begin(); bit != browsers.end(); ++bit) {
    308       if (OpenInBrowser(bit->get())) {
    309         opening_ = true;
    310         return;
    311       }
    312     }
    313   }
    314 }
    315 
    316 bool OpenRemotePageRequest::OpenInBrowser(
    317     DevToolsAndroidBridge::RemoteBrowser* browser) {
    318   if (!browser->IsChrome())
    319     return false;
    320 #if defined(DEBUG_DEVTOOLS)
    321   if (browser->serial() == kLocalSerial)
    322     return false;
    323 #endif  // defined(DEBUG_DEVTOOLS)
    324   browser->Open(url_, base::Bind(&OpenRemotePageRequest::RemotePageOpened,
    325                 base::Unretained(this)));
    326   return true;
    327 }
    328 
    329 void OpenRemotePageRequest::RemotePageOpened(
    330     DevToolsAndroidBridge::RemotePage* page) {
    331   callback_.Run(page);
    332   android_bridge_->RemoveDeviceListListener(this);
    333   delete this;
    334 }
    335 
    336 }  // namespace
    337 
    338 // DevToolsUI -----------------------------------------------------------------
    339 
    340 // static
    341 GURL DevToolsUI::GetProxyURL(const std::string& frontend_url) {
    342   GURL url(frontend_url);
    343   if (!url.is_valid() || url.host() != kRemoteFrontendDomain)
    344     return GURL(kFallbackFrontendURL);
    345   return GURL(base::StringPrintf("%s://%s/%s/%s",
    346               content::kChromeDevToolsScheme,
    347               chrome::kChromeUIDevToolsHost,
    348               chrome::kChromeUIDevToolsRemotePath,
    349               url.path().substr(1).c_str()));
    350 }
    351 
    352 DevToolsUI::DevToolsUI(content::WebUI* web_ui)
    353     : WebUIController(web_ui),
    354       content::WebContentsObserver(web_ui->GetWebContents()),
    355       bindings_(web_ui->GetWebContents()),
    356       weak_factory_(this) {
    357   web_ui->SetBindings(0);
    358   Profile* profile = Profile::FromWebUI(web_ui);
    359   content::URLDataSource::Add(
    360       profile,
    361       new DevToolsDataSource(profile->GetRequestContext()));
    362 }
    363 
    364 DevToolsUI::~DevToolsUI() {
    365 }
    366 
    367 void DevToolsUI::NavigationEntryCommitted(
    368     const content::LoadCommittedDetails& load_details) {
    369   content::NavigationEntry* entry = load_details.entry;
    370   if (!CommandLine::ForCurrentProcess()->HasSwitch(
    371       switches::kEnableDevToolsExperiments)) {
    372     return;
    373   }
    374 
    375   if (entry->GetVirtualURL() == remote_frontend_loading_url_) {
    376     remote_frontend_loading_url_ = GURL();
    377     return;
    378   }
    379 
    380   std::string path = entry->GetVirtualURL().path().substr(1);
    381   if (!StartsWithASCII(path, kRemoteOpenPrefix, false))
    382     return;
    383 
    384   bindings_.Detach();
    385   remote_page_opening_url_ = entry->GetVirtualURL();
    386   new OpenRemotePageRequest(Profile::FromWebUI(web_ui()),
    387                             entry->GetVirtualURL().query(),
    388                             base::Bind(&DevToolsUI::RemotePageOpened,
    389                                        weak_factory_.GetWeakPtr(),
    390                                        entry->GetVirtualURL()));
    391 }
    392 
    393 void DevToolsUI::RemotePageOpened(
    394     const GURL& virtual_url, DevToolsAndroidBridge::RemotePage* page) {
    395   // Already navigated away while connecting to remote device.
    396   if (remote_page_opening_url_ != virtual_url)
    397     return;
    398 
    399   scoped_ptr<DevToolsAndroidBridge::RemotePage> my_page(page);
    400   remote_page_opening_url_ = GURL();
    401 
    402   Profile* profile = Profile::FromWebUI(web_ui());
    403   GURL url = DevToolsUIBindings::ApplyThemeToURL(profile,
    404       DevToolsUI::GetProxyURL(page->GetFrontendURL()));
    405 
    406   content::NavigationController& navigation_controller =
    407       web_ui()->GetWebContents()->GetController();
    408   content::NavigationController::LoadURLParams params(url);
    409   params.should_replace_current_entry = true;
    410   remote_frontend_loading_url_ = virtual_url;
    411   navigation_controller.LoadURLWithParams(params);
    412   navigation_controller.GetPendingEntry()->SetVirtualURL(virtual_url);
    413 
    414   bindings_.AttachTo(page->GetTarget()->GetAgentHost());
    415 }
    416