• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 background 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 * BackgroundBridgeManager maintains an array of BackgroundBridge, indexed by
28 * the associated tab id.
29 */
30function BackgroundBridgeManager() {
31}
32
33BackgroundBridgeManager.prototype = {
34  // Maps a tab id to its associated BackgroundBridge.
35  bridges_: {},
36
37  run: function() {
38    chrome.runtime.onConnect.addListener(this.onConnect_.bind(this));
39
40    chrome.webRequest.onBeforeRequest.addListener(
41        function(details) {
42          if (this.bridges_[details.tabId])
43            return this.bridges_[details.tabId].onInsecureRequest(details.url);
44        }.bind(this),
45        {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']},
46        ['blocking']);
47
48    chrome.webRequest.onBeforeSendHeaders.addListener(
49        function(details) {
50          if (this.bridges_[details.tabId])
51            return this.bridges_[details.tabId].onBeforeSendHeaders(details);
52          else
53            return {requestHeaders: details.requestHeaders};
54        }.bind(this),
55        {urls: ['*://*/*'], types: ['sub_frame']},
56        ['blocking', 'requestHeaders']);
57
58    chrome.webRequest.onHeadersReceived.addListener(
59        function(details) {
60          if (this.bridges_[details.tabId])
61            this.bridges_[details.tabId].onHeadersReceived(details);
62        }.bind(this),
63        {urls: ['*://*/*'], types: ['sub_frame']},
64        ['responseHeaders']);
65
66    chrome.webRequest.onCompleted.addListener(
67        function(details) {
68          if (this.bridges_[details.tabId])
69            this.bridges_[details.tabId].onCompleted(details);
70        }.bind(this),
71        {urls: ['*://*/*'], types: ['sub_frame']},
72        ['responseHeaders']);
73  },
74
75  onConnect_: function(port) {
76    var tabId = this.getTabIdFromPort_(port);
77    if (!this.bridges_[tabId])
78      this.bridges_[tabId] = new BackgroundBridge(tabId);
79    if (port.name == 'authMain') {
80      this.bridges_[tabId].setupForAuthMain(port);
81      port.onDisconnect.addListener(function() {
82        delete this.bridges_[tabId];
83      }.bind(this));
84    } else if (port.name == 'injected') {
85      this.bridges_[tabId].setupForInjected(port);
86    } else {
87      console.error('Unexpected connection, port.name=' + port.name);
88    }
89  },
90
91  getTabIdFromPort_: function(port) {
92    return port.sender.tab ? port.sender.tab.id : -1;
93  }
94};
95
96/**
97 * BackgroundBridge allows the main script and the injected script to
98 * collaborate. It forwards credentials API calls to the main script and
99 * maintains a list of scraped passwords.
100 * @param {string} tabId The associated tab ID.
101 */
102function BackgroundBridge(tabId) {
103  this.tabId_ = tabId;
104}
105
106BackgroundBridge.prototype = {
107  // The associated tab ID. Only used for debugging now.
108  tabId: null,
109
110  isDesktopFlow_: false,
111
112  // Continue URL that is set from main auth script.
113  continueUrl_: null,
114
115  // Whether the extension is loaded in a constrained window.
116  // Set from main auth script.
117  isConstrainedWindow_: null,
118
119  // Email of the newly authenticated user based on the gaia response header
120  // 'google-accounts-signin'.
121  email_: null,
122
123  // Session index of the newly authenticated user based on the gaia response
124  // header 'google-accounts-signin'.
125  sessionIndex_: null,
126
127  // Gaia URL base that is set from main auth script.
128  gaiaUrl_: null,
129
130  // Whether to abort the authentication flow and show an error messagen when
131  // content served over an unencrypted connection is detected.
132  blockInsecureContent_: false,
133
134  // Whether auth flow has started. It is used as a signal of whether the
135  // injected script should scrape passwords.
136  authStarted_: false,
137
138  passwordStore_: {},
139
140  channelMain_: null,
141  channelInjected_: null,
142
143  /**
144   * Sets up the communication channel with the main script.
145   */
146  setupForAuthMain: function(port) {
147    this.channelMain_ = new Channel();
148    this.channelMain_.init(port);
149
150    // Registers for desktop related messages.
151    this.channelMain_.registerMessage(
152        'initDesktopFlow', this.onInitDesktopFlow_.bind(this));
153
154    // Registers for SAML related messages.
155    this.channelMain_.registerMessage(
156        'setGaiaUrl', this.onSetGaiaUrl_.bind(this));
157    this.channelMain_.registerMessage(
158        'setBlockInsecureContent', this.onSetBlockInsecureContent_.bind(this));
159    this.channelMain_.registerMessage(
160        'resetAuth', this.onResetAuth_.bind(this));
161    this.channelMain_.registerMessage(
162        'startAuth', this.onAuthStarted_.bind(this));
163    this.channelMain_.registerMessage(
164        'getScrapedPasswords',
165        this.onGetScrapedPasswords_.bind(this));
166    this.channelMain_.registerMessage(
167        'apiResponse', this.onAPIResponse_.bind(this));
168
169    this.channelMain_.send({
170      'name': 'channelConnected'
171    });
172  },
173
174  /**
175   * Sets up the communication channel with the injected script.
176   */
177  setupForInjected: function(port) {
178    this.channelInjected_ = new Channel();
179    this.channelInjected_.init(port);
180
181    this.channelInjected_.registerMessage(
182        'apiCall', this.onAPICall_.bind(this));
183    this.channelInjected_.registerMessage(
184        'updatePassword', this.onUpdatePassword_.bind(this));
185    this.channelInjected_.registerMessage(
186        'pageLoaded', this.onPageLoaded_.bind(this));
187  },
188
189  /**
190   * Handler for 'initDesktopFlow' signal sent from the main script.
191   * Only called in desktop mode.
192   */
193  onInitDesktopFlow_: function(msg) {
194    this.isDesktopFlow_ = true;
195    this.gaiaUrl_ = msg.gaiaUrl;
196    this.continueUrl_ = msg.continueUrl;
197    this.isConstrainedWindow_ = msg.isConstrainedWindow;
198  },
199
200  /**
201   * Handler for webRequest.onCompleted. It 1) detects loading of continue URL
202   * and notifies the main script of signin completion; 2) detects if the
203   * current page could be loaded in a constrained window and signals the main
204   * script of switching to full tab if necessary.
205   */
206  onCompleted: function(details) {
207    // Only monitors requests in the gaia frame whose parent frame ID must be
208    // positive.
209    if (!this.isDesktopFlow_ || details.parentFrameId <= 0)
210      return;
211
212    var msg = null;
213    if (this.continueUrl_ &&
214        details.url.lastIndexOf(this.continueUrl_, 0) == 0) {
215      var skipForNow = false;
216      if (details.url.indexOf('ntp=1') >= 0)
217        skipForNow = true;
218
219      // TOOD(guohui): Show password confirmation UI.
220      var passwords = this.onGetScrapedPasswords_();
221      msg = {
222        'name': 'completeLogin',
223        'email': this.email_,
224        'password': passwords[0],
225        'sessionIndex': this.sessionIndex_,
226        'skipForNow': skipForNow
227      };
228      this.channelMain_.send(msg);
229    } else if (this.isConstrainedWindow_) {
230      // The header google-accounts-embedded is only set on gaia domain.
231      if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) {
232        var headers = details.responseHeaders;
233        for (var i = 0; headers && i < headers.length; ++i) {
234          if (headers[i].name.toLowerCase() == 'google-accounts-embedded')
235            return;
236        }
237      }
238      msg = {
239        'name': 'switchToFullTab',
240        'url': details.url
241      };
242      this.channelMain_.send(msg);
243    }
244  },
245
246  /**
247   * Handler for webRequest.onBeforeRequest, invoked when content served over an
248   * unencrypted connection is detected. Determines whether the request should
249   * be blocked and if so, signals that an error message needs to be shown.
250   * @param {string} url The URL that was blocked.
251   * @return {!Object} Decision whether to block the request.
252   */
253  onInsecureRequest: function(url) {
254    if (!this.blockInsecureContent_)
255      return {};
256    this.channelMain_.send({name: 'onInsecureContentBlocked', url: url});
257    return {cancel: true};
258  },
259
260  /**
261   * Handler or webRequest.onHeadersReceived. It reads the authenticated user
262   * email from google-accounts-signin-header.
263   */
264  onHeadersReceived: function(details) {
265    if (!this.isDesktopFlow_ ||
266        !this.gaiaUrl_ ||
267        details.url.lastIndexOf(this.gaiaUrl_) != 0) {
268      // TODO(xiyuan, guohui): CrOS should reuse the logic below for reading the
269      // email for SAML users and cut off the /ListAccount call.
270      return;
271    }
272
273    var headers = details.responseHeaders;
274    for (var i = 0; headers && i < headers.length; ++i) {
275      if (headers[i].name.toLowerCase() == 'google-accounts-signin') {
276        var headerValues = headers[i].value.toLowerCase().split(',');
277        var signinDetails = {};
278        headerValues.forEach(function(e) {
279          var pair = e.split('=');
280          signinDetails[pair[0].trim()] = pair[1].trim();
281        });
282        // Remove "" around.
283        this.email_ = signinDetails['email'].slice(1, -1);
284        this.sessionIndex_ = signinDetails['sessionindex'];
285        return;
286      }
287    }
288  },
289
290  /**
291   * Handler for webRequest.onBeforeSendHeaders.
292   * @return {!Object} Modified request headers.
293   */
294  onBeforeSendHeaders: function(details) {
295    if (!this.isDesktopFlow_ && this.gaiaUrl_ &&
296        details.url.indexOf(this.gaiaUrl_) == 0) {
297      details.requestHeaders.push({
298        name: 'X-Cros-Auth-Ext-Support',
299        value: 'SAML'
300      });
301    }
302    return {requestHeaders: details.requestHeaders};
303  },
304
305  /**
306   * Handler for 'setGaiaUrl' signal sent from the main script.
307   */
308  onSetGaiaUrl_: function(msg) {
309    this.gaiaUrl_ = msg.gaiaUrl;
310  },
311
312  /**
313   * Handler for 'setBlockInsecureContent' signal sent from the main script.
314   */
315  onSetBlockInsecureContent_: function(msg) {
316    this.blockInsecureContent_ = msg.blockInsecureContent;
317  },
318
319  /**
320   * Handler for 'resetAuth' signal sent from the main script.
321   */
322  onResetAuth_: function() {
323    this.authStarted_ = false;
324    this.passwordStore_ = {};
325  },
326
327  /**
328   * Handler for 'authStarted' signal sent from the main script.
329   */
330  onAuthStarted_: function() {
331    this.authStarted_ = true;
332    this.passwordStore_ = {};
333  },
334
335  /**
336   * Handler for 'getScrapedPasswords' request sent from the main script.
337   * @return {Array.<string>} The array with de-duped scraped passwords.
338   */
339  onGetScrapedPasswords_: function() {
340    var passwords = {};
341    for (var property in this.passwordStore_) {
342      passwords[this.passwordStore_[property]] = true;
343    }
344    return Object.keys(passwords);
345  },
346
347  /**
348   * Handler for 'apiResponse' signal sent from the main script. Passes on the
349   * |msg| to the injected script.
350   */
351  onAPIResponse_: function(msg) {
352    this.channelInjected_.send(msg);
353  },
354
355  onAPICall_: function(msg) {
356    this.channelMain_.send(msg);
357  },
358
359  onUpdatePassword_: function(msg) {
360    if (!this.authStarted_)
361      return;
362
363    this.passwordStore_[msg.id] = msg.password;
364  },
365
366  onPageLoaded_: function(msg) {
367    if (this.channelMain_)
368      this.channelMain_.send({name: 'onAuthPageLoaded', url: msg.url});
369  }
370};
371
372var backgroundBridgeManager = new BackgroundBridgeManager();
373backgroundBridgeManager.run();
374