Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / gaia_auth / background.js
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  */
30 function BackgroundBridgeManager() {
31 }
32
33 BackgroundBridgeManager.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  */
102 function BackgroundBridge(tabId) {
103   this.tabId_ = tabId;
104 }
105
106 BackgroundBridge.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
372 var backgroundBridgeManager = new BackgroundBridgeManager();
373 backgroundBridgeManager.run();