Upstream version 10.39.225.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   CONTINUE_URL_BASE: 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik' +
35                      '/success.html',
36   // Maps a tab id to its associated BackgroundBridge.
37   bridges_: {},
38
39   run: function() {
40     chrome.runtime.onConnect.addListener(this.onConnect_.bind(this));
41
42     chrome.webRequest.onBeforeRequest.addListener(
43         function(details) {
44           if (this.bridges_[details.tabId])
45             return this.bridges_[details.tabId].onInsecureRequest(details.url);
46         }.bind(this),
47         {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']},
48         ['blocking']);
49
50     chrome.webRequest.onBeforeSendHeaders.addListener(
51         function(details) {
52           if (this.bridges_[details.tabId])
53             return this.bridges_[details.tabId].onBeforeSendHeaders(details);
54           else
55             return {requestHeaders: details.requestHeaders};
56         }.bind(this),
57         {urls: ['*://*/*'], types: ['sub_frame']},
58         ['blocking', 'requestHeaders']);
59
60     chrome.webRequest.onHeadersReceived.addListener(
61         function(details) {
62           if (this.bridges_[details.tabId])
63             return this.bridges_[details.tabId].onHeadersReceived(details);
64         }.bind(this),
65         {urls: ['*://*/*'], types: ['sub_frame']},
66         ['blocking', 'responseHeaders']);
67
68     chrome.webRequest.onCompleted.addListener(
69         function(details) {
70           if (this.bridges_[details.tabId])
71             this.bridges_[details.tabId].onCompleted(details);
72         }.bind(this),
73         {urls: ['*://*/*', this.CONTINUE_URL_BASE + '*'], types: ['sub_frame']},
74         ['responseHeaders']);
75   },
76
77   onConnect_: function(port) {
78     var tabId = this.getTabIdFromPort_(port);
79     if (!this.bridges_[tabId])
80       this.bridges_[tabId] = new BackgroundBridge(tabId);
81     if (port.name == 'authMain') {
82       this.bridges_[tabId].setupForAuthMain(port);
83       port.onDisconnect.addListener(function() {
84         delete this.bridges_[tabId];
85       }.bind(this));
86     } else if (port.name == 'injected') {
87       this.bridges_[tabId].setupForInjected(port);
88     } else {
89       console.error('Unexpected connection, port.name=' + port.name);
90     }
91   },
92
93   getTabIdFromPort_: function(port) {
94     return port.sender.tab ? port.sender.tab.id : -1;
95   }
96 };
97
98 /**
99  * BackgroundBridge allows the main script and the injected script to
100  * collaborate. It forwards credentials API calls to the main script and
101  * maintains a list of scraped passwords.
102  * @param {string} tabId The associated tab ID.
103  */
104 function BackgroundBridge(tabId) {
105   this.tabId_ = tabId;
106 }
107
108 BackgroundBridge.prototype = {
109   // The associated tab ID. Only used for debugging now.
110   tabId: null,
111
112   isDesktopFlow_: false,
113
114   // Whether the extension is loaded in a constrained window.
115   // Set from main auth script.
116   isConstrainedWindow_: null,
117
118   // Email of the newly authenticated user based on the gaia response header
119   // 'google-accounts-signin'.
120   email_: null,
121
122   // Session index of the newly authenticated user based on the gaia response
123   // header 'google-accounts-signin'.
124   sessionIndex_: null,
125
126   // Gaia URL base that is set from main auth script.
127   gaiaUrl_: null,
128
129   // Whether to abort the authentication flow and show an error messagen when
130   // content served over an unencrypted connection is detected.
131   blockInsecureContent_: false,
132
133   // Whether auth flow has started. It is used as a signal of whether the
134   // injected script should scrape passwords.
135   authStarted_: false,
136
137   passwordStore_: {},
138
139   channelMain_: null,
140   channelInjected_: null,
141
142   /**
143    * Sets up the communication channel with the main script.
144    */
145   setupForAuthMain: function(port) {
146     this.channelMain_ = new Channel();
147     this.channelMain_.init(port);
148
149     // Registers for desktop related messages.
150     this.channelMain_.registerMessage(
151         'initDesktopFlow', this.onInitDesktopFlow_.bind(this));
152
153     // Registers for SAML related messages.
154     this.channelMain_.registerMessage(
155         'setGaiaUrl', this.onSetGaiaUrl_.bind(this));
156     this.channelMain_.registerMessage(
157         'setBlockInsecureContent', this.onSetBlockInsecureContent_.bind(this));
158     this.channelMain_.registerMessage(
159         'resetAuth', this.onResetAuth_.bind(this));
160     this.channelMain_.registerMessage(
161         'startAuth', this.onAuthStarted_.bind(this));
162     this.channelMain_.registerMessage(
163         'getScrapedPasswords',
164         this.onGetScrapedPasswords_.bind(this));
165     this.channelMain_.registerMessage(
166         'apiResponse', this.onAPIResponse_.bind(this));
167
168     this.channelMain_.send({
169       'name': 'channelConnected'
170     });
171   },
172
173   /**
174    * Sets up the communication channel with the injected script.
175    */
176   setupForInjected: function(port) {
177     this.channelInjected_ = new Channel();
178     this.channelInjected_.init(port);
179
180     this.channelInjected_.registerMessage(
181         'apiCall', this.onAPICall_.bind(this));
182     this.channelInjected_.registerMessage(
183         'updatePassword', this.onUpdatePassword_.bind(this));
184     this.channelInjected_.registerMessage(
185         'pageLoaded', this.onPageLoaded_.bind(this));
186   },
187
188   /**
189    * Handler for 'initDesktopFlow' signal sent from the main script.
190    * Only called in desktop mode.
191    */
192   onInitDesktopFlow_: function(msg) {
193     this.isDesktopFlow_ = true;
194     this.gaiaUrl_ = msg.gaiaUrl;
195     this.isConstrainedWindow_ = msg.isConstrainedWindow;
196   },
197
198   /**
199    * Handler for webRequest.onCompleted. It 1) detects loading of continue URL
200    * and notifies the main script of signin completion; 2) detects if the
201    * current page could be loaded in a constrained window and signals the main
202    * script of switching to full tab if necessary.
203    */
204   onCompleted: function(details) {
205     // Only monitors requests in the gaia frame whose parent frame ID must be
206     // positive.
207     if (!this.isDesktopFlow_ || details.parentFrameId <= 0)
208       return;
209
210     if (details.url.lastIndexOf(backgroundBridgeManager.CONTINUE_URL_BASE, 0) ==
211         0) {
212       var skipForNow = false;
213       if (details.url.indexOf('ntp=1') >= 0)
214         skipForNow = true;
215
216       // TOOD(guohui): Show password confirmation UI.
217       var passwords = this.onGetScrapedPasswords_();
218       var msg = {
219         'name': 'completeLogin',
220         'email': this.email_,
221         'password': passwords[0],
222         'sessionIndex': this.sessionIndex_,
223         'skipForNow': skipForNow
224       };
225       this.channelMain_.send(msg);
226     } else if (this.isConstrainedWindow_) {
227       // The header google-accounts-embedded is only set on gaia domain.
228       if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) {
229         var headers = details.responseHeaders;
230         for (var i = 0; headers && i < headers.length; ++i) {
231           if (headers[i].name.toLowerCase() == 'google-accounts-embedded')
232             return;
233         }
234       }
235       var msg = {
236         'name': 'switchToFullTab',
237         'url': details.url
238       };
239       this.channelMain_.send(msg);
240     }
241   },
242
243   /**
244    * Handler for webRequest.onBeforeRequest, invoked when content served over an
245    * unencrypted connection is detected. Determines whether the request should
246    * be blocked and if so, signals that an error message needs to be shown.
247    * @param {string} url The URL that was blocked.
248    * @return {!Object} Decision whether to block the request.
249    */
250   onInsecureRequest: function(url) {
251     if (!this.blockInsecureContent_)
252       return {};
253     this.channelMain_.send({name: 'onInsecureContentBlocked', url: url});
254     return {cancel: true};
255   },
256
257   /**
258    * Handler or webRequest.onHeadersReceived. It reads the authenticated user
259    * email from google-accounts-signin-header.
260    * @return {!Object} Modified request headers.
261    */
262   onHeadersReceived: function(details) {
263     var headers = details.responseHeaders;
264
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       for (var i = 0; headers && i < headers.length; ++i) {
271         if (headers[i].name.toLowerCase() == 'google-accounts-signin') {
272           var headerValues = headers[i].value.toLowerCase().split(',');
273           var signinDetails = {};
274           headerValues.forEach(function(e) {
275             var pair = e.split('=');
276             signinDetails[pair[0].trim()] = pair[1].trim();
277           });
278           // Remove "" around.
279           this.email_ = signinDetails['email'].slice(1, -1);
280           this.sessionIndex_ = signinDetails['sessionindex'];
281           break;
282         }
283       }
284     }
285
286     if (!this.isDesktopFlow_) {
287       // Check whether GAIA headers indicating the start or end of a SAML
288       // redirect are present. If so, synthesize cookies to mark these points.
289       for (var i = 0; headers && i < headers.length; ++i) {
290         if (headers[i].name.toLowerCase() == 'google-accounts-saml') {
291           var action = headers[i].value.toLowerCase();
292           if (action == 'start') {
293             // GAIA is redirecting to a SAML IdP. Any cookies contained in the
294             // current |headers| were set by GAIA. Any cookies set in future
295             // requests will be coming from the IdP. Append a cookie to the
296             // current |headers| that marks the point at which the redirect
297             // occurred.
298             headers.push({name: 'Set-Cookie',
299                           value: 'google-accounts-saml-start=now'});
300             return {responseHeaders: headers};
301           } else if (action == 'end') {
302             // The SAML IdP has redirected back to GAIA. Add a cookie that marks
303             // the point at which the redirect occurred occurred. It is
304             // important that this cookie be prepended to the current |headers|
305             // because any cookies contained in the |headers| were already set
306             // by GAIA, not the IdP. Due to limitations in the webRequest API,
307             // it is not trivial to prepend a cookie:
308             //
309             // The webRequest API only allows for deleting and appending
310             // headers. To prepend a cookie (C), three steps are needed:
311             // 1) Delete any headers that set cookies (e.g., A, B).
312             // 2) Append a header which sets the cookie (C).
313             // 3) Append the original headers (A, B).
314             //
315             // Due to a further limitation of the webRequest API, it is not
316             // possible to delete a header in step 1) and append an identical
317             // header in step 3). To work around this, a trailing semicolon is
318             // added to each header before appending it. Trailing semicolons are
319             // ignored by Chrome in cookie headers, causing the modified headers
320             // to actually set the original cookies.
321             var otherHeaders = [];
322             var cookies = [{name: 'Set-Cookie',
323                             value: 'google-accounts-saml-end=now'}];
324             for (var j = 0; j < headers.length; ++j) {
325               if (headers[j].name.toLowerCase().indexOf('set-cookie') == 0) {
326                 var header = headers[j];
327                 header.value += ';';
328                 cookies.push(header);
329               } else {
330                 otherHeaders.push(headers[j]);
331               }
332             }
333             return {responseHeaders: otherHeaders.concat(cookies)};
334           }
335         }
336       }
337     }
338
339     return {};
340   },
341
342   /**
343    * Handler for webRequest.onBeforeSendHeaders.
344    * @return {!Object} Modified request headers.
345    */
346   onBeforeSendHeaders: function(details) {
347     if (!this.isDesktopFlow_ && this.gaiaUrl_ &&
348         details.url.indexOf(this.gaiaUrl_) == 0) {
349       details.requestHeaders.push({
350         name: 'X-Cros-Auth-Ext-Support',
351         value: 'SAML'
352       });
353     }
354     return {requestHeaders: details.requestHeaders};
355   },
356
357   /**
358    * Handler for 'setGaiaUrl' signal sent from the main script.
359    */
360   onSetGaiaUrl_: function(msg) {
361     this.gaiaUrl_ = msg.gaiaUrl;
362   },
363
364   /**
365    * Handler for 'setBlockInsecureContent' signal sent from the main script.
366    */
367   onSetBlockInsecureContent_: function(msg) {
368     this.blockInsecureContent_ = msg.blockInsecureContent;
369   },
370
371   /**
372    * Handler for 'resetAuth' signal sent from the main script.
373    */
374   onResetAuth_: function() {
375     this.authStarted_ = false;
376     this.passwordStore_ = {};
377   },
378
379   /**
380    * Handler for 'authStarted' signal sent from the main script.
381    */
382   onAuthStarted_: function() {
383     this.authStarted_ = true;
384     this.passwordStore_ = {};
385   },
386
387   /**
388    * Handler for 'getScrapedPasswords' request sent from the main script.
389    * @return {Array.<string>} The array with de-duped scraped passwords.
390    */
391   onGetScrapedPasswords_: function() {
392     var passwords = {};
393     for (var property in this.passwordStore_) {
394       passwords[this.passwordStore_[property]] = true;
395     }
396     return Object.keys(passwords);
397   },
398
399   /**
400    * Handler for 'apiResponse' signal sent from the main script. Passes on the
401    * |msg| to the injected script.
402    */
403   onAPIResponse_: function(msg) {
404     this.channelInjected_.send(msg);
405   },
406
407   onAPICall_: function(msg) {
408     this.channelMain_.send(msg);
409   },
410
411   onUpdatePassword_: function(msg) {
412     if (!this.authStarted_)
413       return;
414
415     this.passwordStore_[msg.id] = msg.password;
416   },
417
418   onPageLoaded_: function(msg) {
419     if (this.channelMain_)
420       this.channelMain_.send({name: 'onAuthPageLoaded', url: msg.url});
421   }
422 };
423
424 var backgroundBridgeManager = new BackgroundBridgeManager();
425 backgroundBridgeManager.run();