43cb223ffafe857ba0ce6f758e3959c6828753fd
[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 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  */
31 function BackgroundBridge() {
32 }
33
34 BackgroundBridge.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
291 var backgroundBridge = new BackgroundBridge();
292 backgroundBridge.run();