background.js revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
1// Copyright 2013 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/** 6 * @fileoverview 7 * A background script of the auth extension that bridges the communication 8 * between the main and injected scripts. 9 * 10 * Here is an overview of the communication flow when SAML is being used: 11 * 1. The main script sends the |startAuth| signal to this background script, 12 * indicating that the authentication flow has started and SAML pages may be 13 * loaded from now on. 14 * 2. A script is injected into each SAML page. The injected script sends three 15 * main types of messages to this background script: 16 * a) A |pageLoaded| message is sent when the page has been loaded. This is 17 * forwarded to the main script as |onAuthPageLoaded|. 18 * b) If the SAML provider supports the credential passing API, the API calls 19 * are sent to this backgroudn script as |apiCall| messages. These 20 * messages are forwarded unmodified to the main script. 21 * c) The injected script scrapes passwords. They are sent to this background 22 * script in |updatePassword| messages. The main script can request a list 23 * of the scraped passwords by sending the |getScrapedPasswords| message. 24 */ 25 26/** 27 * BackgroundBridge allows the main script and the injected script to 28 * collaborate. It forwards credentials API calls to the main script and 29 * maintains a list of scraped passwords. 30 */ 31function BackgroundBridge() { 32} 33 34BackgroundBridge.prototype = { 35 // Continue URL that is set from main auth script. 36 continueUrl_: null, 37 38 // Whether the extension is loaded in a constrained window. 39 // Set from main auth script. 40 isConstrainedWindow_: null, 41 42 // Email of the newly authenticated user based on the gaia response header 43 // 'google-accounts-signin'. 44 email_: null, 45 46 // Session index of the newly authenticated user based on the gaia response 47 // header 'google-accounts-signin'. 48 sessionIndex_: null, 49 50 // Gaia URL base that is set from main auth script. 51 gaiaUrl_: null, 52 53 // Whether auth flow has started. It is used as a signal of whether the 54 // injected script should scrape passwords. 55 authStarted_: false, 56 57 passwordStore_: {}, 58 59 channelMain_: {}, 60 channelInjected_: {}, 61 62 run: function() { 63 chrome.runtime.onConnect.addListener(this.onConnect_.bind(this)); 64 65 // Workarounds for loading SAML page in an iframe. 66 chrome.webRequest.onHeadersReceived.addListener( 67 function(details) { 68 if (!this.authStarted_) 69 return; 70 71 var headers = details.responseHeaders; 72 for (var i = 0; headers && i < headers.length; ++i) { 73 if (headers[i].name.toLowerCase() == 'x-frame-options') { 74 headers.splice(i, 1); 75 break; 76 } 77 } 78 return {responseHeaders: headers}; 79 }.bind(this), 80 {urls: ['<all_urls>'], types: ['sub_frame']}, 81 ['blocking', 'responseHeaders']); 82 }, 83 84 onConnect_: function(port) { 85 if (port.name == 'authMain') 86 this.setupForAuthMain_(port); 87 else if (port.name == 'injected') 88 this.setupForInjected_(port); 89 else 90 console.error('Unexpected connection, port.name=' + port.name); 91 }, 92 93 /** 94 * Sets up the communication channel with the main script. 95 */ 96 setupForAuthMain_: function(port) { 97 var currentChannel = new Channel(); 98 currentChannel.init(port); 99 100 // Registers for desktop related messages. 101 currentChannel.registerMessage( 102 'initDesktopFlow', this.onInitDesktopFlow_.bind(this)); 103 104 // Registers for SAML related messages. 105 currentChannel.registerMessage( 106 'setGaiaUrl', this.onSetGaiaUrl_.bind(this)); 107 currentChannel.registerMessage( 108 'resetAuth', this.onResetAuth_.bind(this)); 109 currentChannel.registerMessage( 110 'startAuth', this.onAuthStarted_.bind(this)); 111 currentChannel.registerMessage( 112 'getScrapedPasswords', 113 this.onGetScrapedPasswords_.bind(this)); 114 115 currentChannel.send({ 116 'name': 'channelConnected' 117 }); 118 this.channelMain_[this.getTabIdFromPort_(port)] = currentChannel; 119 }, 120 121 /** 122 * Sets up the communication channel with the injected script. 123 */ 124 setupForInjected_: function(port) { 125 var currentChannel = new Channel(); 126 currentChannel.init(port); 127 128 var tabId = this.getTabIdFromPort_(port); 129 currentChannel.registerMessage( 130 'apiCall', this.onAPICall_.bind(this, tabId)); 131 currentChannel.registerMessage( 132 'updatePassword', this.onUpdatePassword_.bind(this)); 133 currentChannel.registerMessage( 134 'pageLoaded', this.onPageLoaded_.bind(this, tabId)); 135 136 this.channelInjected_[this.getTabIdFromPort_(port)] = currentChannel; 137 }, 138 139 getTabIdFromPort_: function(port) { 140 return port.sender.tab ? port.sender.tab.id : -1; 141 }, 142 143 /** 144 * Handler for 'initDesktopFlow' signal sent from the main script. 145 * Only called in desktop mode. 146 */ 147 onInitDesktopFlow_: function(msg) { 148 this.gaiaUrl_ = msg.gaiaUrl; 149 this.continueUrl_ = msg.continueUrl; 150 this.isConstrainedWindow_ = msg.isConstrainedWindow; 151 152 var urls = []; 153 var filter = {urls: urls, types: ['sub_frame']}; 154 var optExtraInfoSpec = []; 155 if (msg.isConstrainedWindow) { 156 urls.push('<all_urls>'); 157 optExtraInfoSpec.push('responseHeaders'); 158 } else { 159 urls.push(this.continueUrl_ + '*'); 160 } 161 162 chrome.webRequest.onCompleted.addListener( 163 this.onRequestCompletedInDesktopMode_.bind(this), 164 filter, optExtraInfoSpec); 165 chrome.webRequest.onHeadersReceived.addListener( 166 this.onHeadersReceivedInDesktopMode_.bind(this), 167 {urls: [this.gaiaUrl_ + '*'], types: ['sub_frame']}, 168 ['responseHeaders']); 169 }, 170 171 /** 172 * Event listener for webRequest.onCompleted in desktop mode. 173 */ 174 onRequestCompletedInDesktopMode_: function(details) { 175 var msg = null; 176 if (details.url.lastIndexOf(this.continueUrl_, 0) == 0) { 177 var skipForNow = false; 178 if (details.url.indexOf('ntp=1') >= 0) { 179 skipForNow = true; 180 } 181 msg = { 182 'name': 'completeLogin', 183 'email': this.email_, 184 'sessionIndex': this.sessionIndex_, 185 'skipForNow': skipForNow 186 }; 187 } else if (this.isConstrainedWindow_) { 188 var headers = details.responseHeaders; 189 for (var i = 0; headers && i < headers.length; ++i) { 190 if (headers[i].name.toLowerCase() == 'google-accounts-embedded') { 191 return; 192 } 193 } 194 msg = { 195 'name': 'switchToFullTab', 196 'url': details.url 197 }; 198 } 199 200 if (msg != null) 201 this.channelMain_[details.tabId].send(msg); 202 }, 203 204 /** 205 * Event listener for webRequest.onHeadersReceived in desktop mode. 206 */ 207 onHeadersReceivedInDesktopMode_: function(details) { 208 var headers = details.responseHeaders; 209 for (var i = 0; headers && i < headers.length; ++i) { 210 if (headers[i].name.toLowerCase() == 'google-accounts-signin') { 211 var headerValues = headers[i].value.toLowerCase().split(','); 212 var signinDetails = {}; 213 headerValues.forEach(function(e) { 214 var pair = e.split('='); 215 signinDetails[pair[0].trim()] = pair[1].trim(); 216 }); 217 this.email_ = signinDetails['email'].slice(1, -1); // Remove "" around. 218 this.sessionIndex_ = signinDetails['sessionindex']; 219 return; 220 } 221 } 222 }, 223 224 /** 225 * Handler for 'setGaiaUrl' signal sent from the main script. 226 */ 227 onSetGaiaUrl_: function(msg) { 228 this.gaiaUrl_ = msg.gaiaUrl; 229 230 // Set request header to let Gaia know that saml support is on. 231 chrome.webRequest.onBeforeSendHeaders.addListener( 232 function(details) { 233 details.requestHeaders.push({ 234 name: 'X-Cros-Auth-Ext-Support', 235 value: 'SAML' 236 }); 237 return {requestHeaders: details.requestHeaders}; 238 }, 239 {urls: [this.gaiaUrl_ + '*'], types: ['sub_frame']}, 240 ['blocking', 'requestHeaders']); 241 }, 242 243 /** 244 * Handler for 'resetAuth' signal sent from the main script. 245 */ 246 onResetAuth_: function() { 247 this.authStarted_ = false; 248 this.passwordStore_ = {}; 249 }, 250 251 /** 252 * Handler for 'authStarted' signal sent from the main script. 253 */ 254 onAuthStarted_: function() { 255 this.authStarted_ = true; 256 this.passwordStore_ = {}; 257 }, 258 259 /** 260 * Handler for 'getScrapedPasswords' request sent from the main script. 261 * @return {Array.<string>} The array with de-duped scraped passwords. 262 */ 263 onGetScrapedPasswords_: function() { 264 var passwords = {}; 265 for (var property in this.passwordStore_) { 266 passwords[this.passwordStore_[property]] = true; 267 } 268 return Object.keys(passwords); 269 }, 270 271 onAPICall_: function(tabId, msg) { 272 if (tabId in this.channelMain_) { 273 this.channelMain_[tabId].send(msg); 274 } 275 }, 276 277 onUpdatePassword_: function(msg) { 278 if (!this.authStarted_) 279 return; 280 281 this.passwordStore_[msg.id] = msg.password; 282 }, 283 284 onPageLoaded_: function(tabId, msg) { 285 if (tabId in this.channelMain_) { 286 this.channelMain_[tabId].send({name: 'onAuthPageLoaded', url: msg.url}); 287 } 288 } 289}; 290 291var backgroundBridge = new BackgroundBridge(); 292backgroundBridge.run(); 293