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