Source code

Revision control

Other Tools

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
const { XPCOMUtils } = ChromeUtils.import(
7
);
8
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9
const { PrivateBrowsingUtils } = ChromeUtils.import(
11
);
12
const { PromptUtils } = ChromeUtils.import(
14
);
15
16
/* eslint-disable block-scoped-var, no-var */
17
18
ChromeUtils.defineModuleGetter(
19
this,
20
"LoginHelper",
22
);
23
ChromeUtils.defineModuleGetter(
24
this,
25
"LoginManagerPrompter",
27
);
28
29
const LoginInfo = Components.Constructor(
30
"@mozilla.org/login-manager/loginInfo;1",
31
"nsILoginInfo",
32
"init"
33
);
34
35
/**
36
* A helper module to prevent modal auth prompt abuse.
37
*/
38
const PromptAbuseHelper = {
39
getBaseDomainOrFallback(hostname) {
40
try {
41
return Services.eTLD.getBaseDomainFromHost(hostname);
42
} catch (e) {
43
return hostname;
44
}
45
},
46
47
incrementPromptAbuseCounter(baseDomain, browser) {
48
if (!browser) {
49
return;
50
}
51
52
if (!browser.authPromptAbuseCounter) {
53
browser.authPromptAbuseCounter = {};
54
}
55
56
if (!browser.authPromptAbuseCounter[baseDomain]) {
57
browser.authPromptAbuseCounter[baseDomain] = 0;
58
}
59
60
browser.authPromptAbuseCounter[baseDomain] += 1;
61
},
62
63
resetPromptAbuseCounter(baseDomain, browser) {
64
if (!browser || !browser.authPromptAbuseCounter) {
65
return;
66
}
67
68
browser.authPromptAbuseCounter[baseDomain] = 0;
69
},
70
71
hasReachedAbuseLimit(baseDomain, browser) {
72
if (!browser || !browser.authPromptAbuseCounter) {
73
return false;
74
}
75
76
let abuseCounter = browser.authPromptAbuseCounter[baseDomain];
77
// Allow for setting -1 to turn the feature off.
78
if (this.abuseLimit < 0) {
79
return false;
80
}
81
return !!abuseCounter && abuseCounter >= this.abuseLimit;
82
},
83
};
84
85
XPCOMUtils.defineLazyPreferenceGetter(
86
PromptAbuseHelper,
87
"abuseLimit",
88
"prompts.authentication_dialog_abuse_limit"
89
);
90
91
/**
92
* Implements nsIPromptFactory
93
*
94
* Invoked by [toolkit/components/prompts/src/Prompter.jsm]
95
*/
96
function LoginManagerAuthPromptFactory() {
97
Services.obs.addObserver(this, "quit-application-granted", true);
98
Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
99
Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true);
100
}
101
102
LoginManagerAuthPromptFactory.prototype = {
103
classID: Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
104
QueryInterface: ChromeUtils.generateQI([
105
Ci.nsIPromptFactory,
106
Ci.nsIObserver,
107
Ci.nsISupportsWeakReference,
108
]),
109
110
_asyncPrompts: {},
111
_asyncPromptInProgress: false,
112
113
observe(subject, topic, data) {
114
this.log("Observed: " + topic);
115
if (topic == "quit-application-granted") {
116
this._cancelPendingPrompts();
117
} else if (topic == "passwordmgr-crypto-login") {
118
// Start processing the deferred prompters.
119
this._doAsyncPrompt();
120
} else if (topic == "passwordmgr-crypto-loginCanceled") {
121
// User canceled a Master Password prompt, so go ahead and cancel
122
// all pending auth prompts to avoid nagging over and over.
123
this._cancelPendingPrompts();
124
}
125
},
126
127
getPrompt(aWindow, aIID) {
128
var prompt = new LoginManagerAuthPrompter().QueryInterface(aIID);
129
prompt.init(aWindow, this);
130
return prompt;
131
},
132
133
_doAsyncPrompt() {
134
if (this._asyncPromptInProgress) {
135
this.log("_doAsyncPrompt bypassed, already in progress");
136
return;
137
}
138
139
// Find the first prompt key we have in the queue
140
var hashKey = null;
141
for (hashKey in this._asyncPrompts) {
142
break;
143
}
144
145
if (!hashKey) {
146
this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
147
return;
148
}
149
150
// If login manger has logins for this host, defer prompting if we're
151
// already waiting on a master password entry.
152
var prompt = this._asyncPrompts[hashKey];
153
var prompter = prompt.prompter;
154
var [origin, httpRealm] = prompter._getAuthTarget(
155
prompt.channel,
156
prompt.authInfo
157
);
158
var hasLogins = Services.logins.countLogins(origin, null, httpRealm) > 0;
159
if (
160
!hasLogins &&
161
LoginHelper.schemeUpgrades &&
162
origin.startsWith("https://")
163
) {
164
let httpOrigin = origin.replace(/^https:\/\//, "http://");
165
hasLogins = Services.logins.countLogins(httpOrigin, null, httpRealm) > 0;
166
}
167
if (hasLogins && Services.logins.uiBusy) {
168
this.log("_doAsyncPrompt:run bypassed, master password UI busy");
169
return;
170
}
171
172
var self = this;
173
174
var runnable = {
175
cancel: false,
176
run() {
177
var ok = false;
178
if (!this.cancel) {
179
try {
180
self.log(
181
"_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'"
182
);
183
ok = prompter.promptAuth(
184
prompt.channel,
185
prompt.level,
186
prompt.authInfo
187
);
188
} catch (e) {
189
if (
190
e instanceof Components.Exception &&
191
e.result == Cr.NS_ERROR_NOT_AVAILABLE
192
) {
193
self.log(
194
"_doAsyncPrompt:run bypassed, UI is not available in this context"
195
);
196
} else {
197
Cu.reportError(
198
"LoginManagerAuthPrompter: _doAsyncPrompt:run: " + e + "\n"
199
);
200
}
201
}
202
203
delete self._asyncPrompts[hashKey];
204
prompt.inProgress = false;
205
self._asyncPromptInProgress = false;
206
}
207
208
for (var consumer of prompt.consumers) {
209
if (!consumer.callback) {
210
// Not having a callback means that consumer didn't provide it
211
// or canceled the notification
212
continue;
213
}
214
215
self.log("Calling back to " + consumer.callback + " ok=" + ok);
216
try {
217
if (ok) {
218
consumer.callback.onAuthAvailable(
219
consumer.context,
220
prompt.authInfo
221
);
222
} else {
223
consumer.callback.onAuthCancelled(consumer.context, !this.cancel);
224
}
225
} catch (e) {
226
/* Throw away exceptions caused by callback */
227
}
228
}
229
self._doAsyncPrompt();
230
},
231
};
232
233
this._asyncPromptInProgress = true;
234
prompt.inProgress = true;
235
236
Services.tm.dispatchToMainThread(runnable);
237
this.log("_doAsyncPrompt:run dispatched");
238
},
239
240
_cancelPendingPrompts() {
241
this.log("Canceling all pending prompts...");
242
var asyncPrompts = this._asyncPrompts;
243
this.__proto__._asyncPrompts = {};
244
245
for (var hashKey in asyncPrompts) {
246
let prompt = asyncPrompts[hashKey];
247
// Watch out! If this prompt is currently prompting, let it handle
248
// notifying the callbacks of success/failure, since it's already
249
// asking the user for input. Reusing a callback can be crashy.
250
if (prompt.inProgress) {
251
this.log("skipping a prompt in progress");
252
continue;
253
}
254
255
for (var consumer of prompt.consumers) {
256
if (!consumer.callback) {
257
continue;
258
}
259
260
this.log("Canceling async auth prompt callback " + consumer.callback);
261
try {
262
consumer.callback.onAuthCancelled(consumer.context, true);
263
} catch (e) {
264
/* Just ignore exceptions from the callback */
265
}
266
}
267
}
268
},
269
}; // end of LoginManagerAuthPromptFactory implementation
270
271
XPCOMUtils.defineLazyGetter(
272
LoginManagerAuthPromptFactory.prototype,
273
"log",
274
() => {
275
let logger = LoginHelper.createLogger("LoginManagerAuthPromptFactory");
276
return logger.log.bind(logger);
277
}
278
);
279
280
/* ==================== LoginManagerAuthPrompter ==================== */
281
282
/**
283
* Implements interfaces for prompting the user to enter/save/change auth info.
284
*
285
* nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
286
*
287
* nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
288
* (eg HTTP Authenticate, FTP login).
289
*
290
* nsILoginManagerAuthPrompter: Used by consumers to indicate which tab/window a
291
* prompt should appear on.
292
*/
293
function LoginManagerAuthPrompter() {}
294
295
LoginManagerAuthPrompter.prototype = {
296
classID: Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
297
QueryInterface: ChromeUtils.generateQI([
298
Ci.nsIAuthPrompt,
299
Ci.nsIAuthPrompt2,
300
Ci.nsILoginManagerAuthPrompter,
301
]),
302
303
_factory: null,
304
_chromeWindow: null,
305
_browser: null,
306
_openerBrowser: null,
307
308
__strBundle: null, // String bundle for L10N
309
get _strBundle() {
310
if (!this.__strBundle) {
311
this.__strBundle = Services.strings.createBundle(
313
);
314
if (!this.__strBundle) {
315
throw new Error("String bundle for Login Manager not present!");
316
}
317
}
318
319
return this.__strBundle;
320
},
321
322
__ellipsis: null,
323
get _ellipsis() {
324
if (!this.__ellipsis) {
325
this.__ellipsis = "\u2026";
326
try {
327
this.__ellipsis = Services.prefs.getComplexValue(
328
"intl.ellipsis",
329
Ci.nsIPrefLocalizedString
330
).data;
331
} catch (e) {}
332
}
333
return this.__ellipsis;
334
},
335
336
// Whether we are in private browsing mode
337
get _inPrivateBrowsing() {
338
if (this._chromeWindow) {
339
return PrivateBrowsingUtils.isWindowPrivate(this._chromeWindow);
340
}
341
// If we don't that we're in private browsing mode if the caller did
342
// not provide a window. The callers which really care about this
343
// will indeed pass down a window to us, and for those who don't,
344
// we can just assume that we don't want to save the entered login
345
// information.
346
this.log("We have no chromeWindow so assume we're in a private context");
347
return true;
348
},
349
350
get _allowRememberLogin() {
351
if (!this._inPrivateBrowsing) {
352
return true;
353
}
354
return LoginHelper.privateBrowsingCaptureEnabled;
355
},
356
357
/* ---------- nsIAuthPrompt prompts ---------- */
358
359
/**
360
* Wrapper around the prompt service prompt. Saving random fields here
361
* doesn't really make sense and therefore isn't implemented.
362
*/
363
prompt(
364
aDialogTitle,
365
aText,
366
aPasswordRealm,
367
aSavePassword,
368
aDefaultText,
369
aResult
370
) {
371
if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) {
372
throw new Components.Exception(
373
"prompt only supports SAVE_PASSWORD_NEVER",
374
Cr.NS_ERROR_NOT_IMPLEMENTED
375
);
376
}
377
378
this.log("===== prompt() called =====");
379
380
if (aDefaultText) {
381
aResult.value = aDefaultText;
382
}
383
384
return Services.prompt.prompt(
385
this._chromeWindow,
386
aDialogTitle,
387
aText,
388
aResult,
389
null,
390
{}
391
);
392
},
393
394
/**
395
* Looks up a username and password in the database. Will prompt the user
396
* with a dialog, even if a username and password are found.
397
*/
398
promptUsernameAndPassword(
399
aDialogTitle,
400
aText,
401
aPasswordRealm,
402
aSavePassword,
403
aUsername,
404
aPassword
405
) {
406
this.log("===== promptUsernameAndPassword() called =====");
407
408
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
409
throw new Components.Exception(
410
"promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
411
Cr.NS_ERROR_NOT_IMPLEMENTED
412
);
413
}
414
415
let foundLogins = null;
416
var selectedLogin = null;
417
var checkBox = { value: false };
418
var checkBoxLabel = null;
419
var [origin, realm, unused] = this._getRealmInfo(aPasswordRealm);
420
421
// If origin is null, we can't save this login.
422
if (origin) {
423
var canRememberLogin = false;
424
if (this._allowRememberLogin) {
425
canRememberLogin =
426
aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
427
Services.logins.getLoginSavingEnabled(origin);
428
}
429
430
// if checkBoxLabel is null, the checkbox won't be shown at all.
431
if (canRememberLogin) {
432
checkBoxLabel = this._getLocalizedString("rememberPassword");
433
}
434
435
// Look for existing logins.
436
foundLogins = Services.logins.findLogins(origin, null, realm);
437
438
// XXX Like the original code, we can't deal with multiple
439
// account selection. (bug 227632)
440
if (foundLogins.length) {
441
selectedLogin = foundLogins[0];
442
443
// If the caller provided a username, try to use it. If they
444
// provided only a password, this will try to find a password-only
445
// login (or return null if none exists).
446
if (aUsername.value) {
447
selectedLogin = this._repickSelectedLogin(
448
foundLogins,
449
aUsername.value
450
);
451
}
452
453
if (selectedLogin) {
454
checkBox.value = true;
455
aUsername.value = selectedLogin.username;
456
// If the caller provided a password, prefer it.
457
if (!aPassword.value) {
458
aPassword.value = selectedLogin.password;
459
}
460
}
461
}
462
}
463
464
var ok = Services.prompt.promptUsernameAndPassword(
465
this._chromeWindow,
466
aDialogTitle,
467
aText,
468
aUsername,
469
aPassword,
470
checkBoxLabel,
471
checkBox
472
);
473
474
if (!ok || !checkBox.value || !origin) {
475
return ok;
476
}
477
478
if (!aPassword.value) {
479
this.log("No password entered, so won't offer to save.");
480
return ok;
481
}
482
483
// XXX We can't prompt with multiple logins yet (bug 227632), so
484
// the entered login might correspond to an existing login
485
// other than the one we originally selected.
486
selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
487
488
// If we didn't find an existing login, or if the username
489
// changed, save as a new login.
490
let newLogin = new LoginInfo(
491
origin,
492
null,
493
realm,
494
aUsername.value,
495
aPassword.value
496
);
497
if (!selectedLogin) {
498
// add as new
499
this.log("New login seen for " + realm);
500
Services.logins.addLogin(newLogin);
501
} else if (aPassword.value != selectedLogin.password) {
502
// update password
503
this.log("Updating password for " + realm);
504
this._updateLogin(selectedLogin, newLogin);
505
} else {
506
this.log("Login unchanged, no further action needed.");
507
Services.logins.recordPasswordUse(selectedLogin);
508
}
509
510
return ok;
511
},
512
513
/**
514
* If a password is found in the database for the password realm, it is
515
* returned straight away without displaying a dialog.
516
*
517
* If a password is not found in the database, the user will be prompted
518
* with a dialog with a text field and ok/cancel buttons. If the user
519
* allows it, then the password will be saved in the database.
520
*/
521
promptPassword(
522
aDialogTitle,
523
aText,
524
aPasswordRealm,
525
aSavePassword,
526
aPassword
527
) {
528
this.log("===== promptPassword called() =====");
529
530
if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
531
throw new Components.Exception(
532
"promptPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
533
Cr.NS_ERROR_NOT_IMPLEMENTED
534
);
535
}
536
537
var checkBox = { value: false };
538
var checkBoxLabel = null;
539
var [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
540
541
username = decodeURIComponent(username);
542
543
// If origin is null, we can't save this login.
544
if (origin && !this._inPrivateBrowsing) {
545
var canRememberLogin =
546
aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
547
Services.logins.getLoginSavingEnabled(origin);
548
549
// if checkBoxLabel is null, the checkbox won't be shown at all.
550
if (canRememberLogin) {
551
checkBoxLabel = this._getLocalizedString("rememberPassword");
552
}
553
554
if (!aPassword.value) {
555
// Look for existing logins.
556
var foundLogins = Services.logins.findLogins(origin, null, realm);
557
558
// XXX Like the original code, we can't deal with multiple
559
// account selection (bug 227632). We can deal with finding the
560
// account based on the supplied username - but in this case we'll
561
// just return the first match.
562
for (var i = 0; i < foundLogins.length; ++i) {
563
if (foundLogins[i].username == username) {
564
aPassword.value = foundLogins[i].password;
565
// wallet returned straight away, so this mimics that code
566
return true;
567
}
568
}
569
}
570
}
571
572
var ok = Services.prompt.promptPassword(
573
this._chromeWindow,
574
aDialogTitle,
575
aText,
576
aPassword,
577
checkBoxLabel,
578
checkBox
579
);
580
581
if (ok && checkBox.value && origin && aPassword.value) {
582
let newLogin = new LoginInfo(
583
origin,
584
null,
585
realm,
586
username,
587
aPassword.value
588
);
589
590
this.log("New login seen for " + realm);
591
592
Services.logins.addLogin(newLogin);
593
}
594
595
return ok;
596
},
597
598
/* ---------- nsIAuthPrompt helpers ---------- */
599
600
/**
601
* Given aRealmString, such as "http://user@example.com/foo", returns an
602
* array of:
603
* - the formatted origin
604
* - the realm (origin + path)
605
* - the username, if present
606
*
607
* If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
608
* channels, e.g. "example.com:80 (httprealm)", null is returned for all
609
* arguments to let callers know the login can't be saved because we don't
610
* know whether it's http or https.
611
*/
612
_getRealmInfo(aRealmString) {
613
var httpRealm = /^.+ \(.+\)$/;
614
if (httpRealm.test(aRealmString)) {
615
return [null, null, null];
616
}
617
618
var uri = Services.io.newURI(aRealmString);
619
var pathname = "";
620
621
if (uri.pathQueryRef != "/") {
622
pathname = uri.pathQueryRef;
623
}
624
625
var formattedOrigin = this._getFormattedOrigin(uri);
626
627
return [formattedOrigin, formattedOrigin + pathname, uri.username];
628
},
629
630
/* ---------- nsIAuthPrompt2 prompts ---------- */
631
632
/**
633
* Implementation of nsIAuthPrompt2.
634
*
635
* @param {nsIChannel} aChannel
636
* @param {int} aLevel
637
* @param {nsIAuthInformation} aAuthInfo
638
*/
639
promptAuth(aChannel, aLevel, aAuthInfo) {
640
var selectedLogin = null;
641
var checkbox = { value: false };
642
var checkboxLabel = null;
643
var epicfail = false;
644
var canAutologin = false;
645
var notifyObj;
646
var foundLogins;
647
648
try {
649
this.log("===== promptAuth called =====");
650
651
// If the user submits a login but it fails, we need to remove the
652
// notification prompt that was displayed. Conveniently, the user will
653
// be prompted for authentication again, which brings us here.
654
this._removeLoginNotifications();
655
656
var [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
657
658
// Looks for existing logins to prefill the prompt with.
659
foundLogins = LoginHelper.searchLoginsWithObject({
660
origin,
661
httpRealm,
662
schemeUpgrades: LoginHelper.schemeUpgrades,
663
});
664
this.log("found", foundLogins.length, "matching logins.");
665
let resolveBy = ["scheme", "timePasswordChanged"];
666
foundLogins = LoginHelper.dedupeLogins(
667
foundLogins,
668
["username"],
669
resolveBy,
670
origin
671
);
672
this.log(foundLogins.length, "matching logins remain after deduping");
673
674
// XXX Can't select from multiple accounts yet. (bug 227632)
675
if (foundLogins.length) {
676
selectedLogin = foundLogins[0];
677
this._SetAuthInfo(
678
aAuthInfo,
679
selectedLogin.username,
680
selectedLogin.password
681
);
682
683
// Allow automatic proxy login
684
if (
685
aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
686
!(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
687
Services.prefs.getBoolPref("signon.autologin.proxy") &&
688
!this._inPrivateBrowsing
689
) {
690
this.log("Autologin enabled, skipping auth prompt.");
691
canAutologin = true;
692
}
693
694
checkbox.value = true;
695
}
696
697
var canRememberLogin = Services.logins.getLoginSavingEnabled(origin);
698
if (!this._allowRememberLogin) {
699
canRememberLogin = false;
700
}
701
702
// if checkboxLabel is null, the checkbox won't be shown at all.
703
notifyObj = this._getPopupNote();
704
if (canRememberLogin && !notifyObj) {
705
checkboxLabel = this._getLocalizedString("rememberPassword");
706
}
707
} catch (e) {
708
// Ignore any errors and display the prompt anyway.
709
epicfail = true;
710
Cu.reportError(
711
"LoginManagerAuthPrompter: Epic fail in promptAuth: " + e + "\n"
712
);
713
}
714
715
var ok = canAutologin;
716
let browser = this._browser;
717
let baseDomain;
718
719
// We might not have a browser or browser.currentURI.host could fail
720
// (e.g. on about:blank). Fall back to the subresource hostname in that case.
721
try {
722
let topLevelHost = browser.currentURI.host;
723
baseDomain = PromptAbuseHelper.getBaseDomainOrFallback(topLevelHost);
724
} catch (e) {
725
baseDomain = PromptAbuseHelper.getBaseDomainOrFallback(origin);
726
}
727
728
if (!ok) {
729
if (PromptAbuseHelper.hasReachedAbuseLimit(baseDomain, browser)) {
730
this.log("Blocking auth dialog, due to exceeding dialog bloat limit");
731
return false;
732
}
733
734
// Set up a counter for ensuring that the basic auth prompt can not
735
// be abused for DOS-style attacks. With this counter, each eTLD+1
736
// per browser will get a limited number of times a user can
737
// cancel the prompt until we stop showing it.
738
PromptAbuseHelper.incrementPromptAbuseCounter(baseDomain, browser);
739
740
if (this._chromeWindow) {
741
PromptUtils.fireDialogEvent(
742
this._chromeWindow,
743
"DOMWillOpenModalDialog",
744
this._browser
745
);
746
}
747
ok = Services.prompt.promptAuth(
748
this._chromeWindow,
749
aChannel,
750
aLevel,
751
aAuthInfo,
752
checkboxLabel,
753
checkbox
754
);
755
}
756
757
let [username, password] = this._GetAuthInfo(aAuthInfo);
758
759
// Reset the counter state if the user replied to a prompt and actually
760
// tried to login (vs. simply clicking any button to get out).
761
if (ok && (username || password)) {
762
PromptAbuseHelper.resetPromptAbuseCounter(baseDomain, browser);
763
}
764
765
// If there's a notification prompt, use it to allow the user to
766
// determine if the login should be saved. If there isn't a
767
// notification prompt, only save the login if the user set the
768
// checkbox to do so.
769
var rememberLogin = notifyObj ? canRememberLogin : checkbox.value;
770
if (!ok || !rememberLogin || epicfail) {
771
return ok;
772
}
773
774
try {
775
if (!password) {
776
this.log("No password entered, so won't offer to save.");
777
return ok;
778
}
779
780
// XXX We can't prompt with multiple logins yet (bug 227632), so
781
// the entered login might correspond to an existing login
782
// other than the one we originally selected.
783
selectedLogin = this._repickSelectedLogin(foundLogins, username);
784
785
// If we didn't find an existing login, or if the username
786
// changed, save as a new login.
787
let newLogin = new LoginInfo(origin, null, httpRealm, username, password);
788
if (!selectedLogin) {
789
this.log(
790
"New login seen for " +
791
username +
792
" @ " +
793
origin +
794
" (" +
795
httpRealm +
796
")"
797
);
798
799
if (notifyObj) {
800
let promptBrowser = LoginHelper.getBrowserForPrompt(browser);
801
LoginManagerPrompter._showLoginCaptureDoorhanger(
802
promptBrowser,
803
newLogin,
804
"password-save",
805
{
806
dismissed: this._inPrivateBrowsing,
807
}
808
);
809
Services.obs.notifyObservers(newLogin, "passwordmgr-prompt-save");
810
} else {
811
Services.logins.addLogin(newLogin);
812
}
813
} else if (password != selectedLogin.password) {
814
this.log(
815
"Updating password for " +
816
username +
817
" @ " +
818
origin +
819
" (" +
820
httpRealm +
821
")"
822
);
823
if (notifyObj) {
824
this._showChangeLoginNotification(browser, selectedLogin, newLogin);
825
} else {
826
this._updateLogin(selectedLogin, newLogin);
827
}
828
} else {
829
this.log("Login unchanged, no further action needed.");
830
Services.logins.recordPasswordUse(selectedLogin);
831
}
832
} catch (e) {
833
Cu.reportError("LoginManagerAuthPrompter: Fail2 in promptAuth: " + e);
834
}
835
836
return ok;
837
},
838
839
asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) {
840
var cancelable = null;
841
842
try {
843
this.log("===== asyncPromptAuth called =====");
844
845
// If the user submits a login but it fails, we need to remove the
846
// notification prompt that was displayed. Conveniently, the user will
847
// be prompted for authentication again, which brings us here.
848
this._removeLoginNotifications();
849
850
cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
851
852
var [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
853
854
var hashKey = aLevel + "|" + origin + "|" + httpRealm;
855
this.log("Async prompt key = " + hashKey);
856
var asyncPrompt = this._factory._asyncPrompts[hashKey];
857
if (asyncPrompt) {
858
this.log(
859
"Prompt bound to an existing one in the queue, callback = " +
860
aCallback
861
);
862
asyncPrompt.consumers.push(cancelable);
863
return cancelable;
864
}
865
866
this.log("Adding new prompt to the queue, callback = " + aCallback);
867
asyncPrompt = {
868
consumers: [cancelable],
869
channel: aChannel,
870
authInfo: aAuthInfo,
871
level: aLevel,
872
inProgress: false,
873
prompter: this,
874
};
875
876
this._factory._asyncPrompts[hashKey] = asyncPrompt;
877
this._factory._doAsyncPrompt();
878
} catch (e) {
879
Cu.reportError(
880
"LoginManagerAuthPrompter: " +
881
"asyncPromptAuth: " +
882
e +
883
"\nFalling back to promptAuth\n"
884
);
885
// Fail the prompt operation to let the consumer fall back
886
// to synchronous promptAuth method
887
throw e;
888
}
889
890
return cancelable;
891
},
892
893
/* ---------- nsILoginManagerAuthPrompter prompts ---------- */
894
895
init(aWindow = null, aFactory = null) {
896
if (!aWindow) {
897
// There may be no applicable window e.g. in a Sandbox or JSM.
898
this._chromeWindow = null;
899
this._browser = null;
900
} else if (aWindow.isChromeWindow) {
901
this._chromeWindow = aWindow;
902
// needs to be set explicitly using setBrowser
903
this._browser = null;
904
} else {
905
let { win, browser } = this._getChromeWindow(aWindow);
906
this._chromeWindow = win;
907
this._browser = browser;
908
}
909
this._openerBrowser = null;
910
this._factory = aFactory || null;
911
912
this.log("===== initialized =====");
913
},
914
915
set browser(aBrowser) {
916
this._browser = aBrowser;
917
},
918
919
set openerBrowser(aOpenerBrowser) {
920
this._openerBrowser = aOpenerBrowser;
921
},
922
923
_removeLoginNotifications() {
924
var popupNote = this._getPopupNote();
925
if (popupNote) {
926
popupNote = popupNote.getNotification("password");
927
}
928
if (popupNote) {
929
popupNote.remove();
930
}
931
},
932
933
/**
934
* Called when we detect a new login in a form submission,
935
* asks the user what to do.
936
*/
937
_showSaveLoginDialog(aLogin) {
938
const buttonFlags =
939
Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
940
Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
941
Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 +
942
Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2;
943
944
var displayHost = this._getShortDisplayHost(aLogin.origin);
945
946
var dialogText;
947
if (aLogin.username) {
948
var displayUser = this._sanitizeUsername(aLogin.username);
949
dialogText = this._getLocalizedString("rememberPasswordMsg", [
950
displayUser,
951
displayHost,
952
]);
953
} else {
954
dialogText = this._getLocalizedString("rememberPasswordMsgNoUsername", [
955
displayHost,
956
]);
957
}
958
var dialogTitle = this._getLocalizedString("savePasswordTitle");
959
var neverButtonText = this._getLocalizedString("neverForSiteButtonText");
960
var rememberButtonText = this._getLocalizedString("rememberButtonText");
961
var notNowButtonText = this._getLocalizedString("notNowButtonText");
962
963
this.log("Prompting user to save/ignore login");
964
var userChoice = Services.prompt.confirmEx(
965
this._chromeWindow,
966
dialogTitle,
967
dialogText,
968
buttonFlags,
969
rememberButtonText,
970
notNowButtonText,
971
neverButtonText,
972
null,
973
{}
974
);
975
// Returns:
976
// 0 - Save the login
977
// 1 - Ignore the login this time
978
// 2 - Never save logins for this site
979
if (userChoice == 2) {
980
this.log("Disabling " + aLogin.origin + " logins by request.");
981
Services.logins.setLoginSavingEnabled(aLogin.origin, false);
982
} else if (userChoice == 0) {
983
this.log("Saving login for " + aLogin.origin);
984
Services.logins.addLogin(aLogin);
985
} else {
986
// userChoice == 1 --> just ignore the login.
987
this.log("Ignoring login.");
988
}
989
990
Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save");
991
},
992
993
/**
994
* Shows the Change Password popup notification.
995
*
996
* @param aBrowser
997
* The relevant <browser>.
998
* @param aOldLogin
999
* The stored login we want to update.
1000
* @param aNewLogin
1001
* The login object with the changes we want to make.
1002
* @param dismissed
1003
* A boolean indicating if the prompt should be automatically
1004
* dismissed on being shown.
1005
* @param notifySaved
1006
* A boolean value indicating whether the notification should indicate that
1007
* a login has been saved
1008
*/
1009
_showChangeLoginNotification(
1010
aBrowser,
1011
aOldLogin,
1012
aNewLogin,
1013
dismissed = false,
1014
notifySaved = false,
1015
autoSavedLoginGuid = ""
1016
) {
1017
let login = aOldLogin.clone();
1018
login.origin = aNewLogin.origin;
1019
login.formActionOrigin = aNewLogin.formActionOrigin;
1020
login.password = aNewLogin.password;
1021
login.username = aNewLogin.username;
1022
1023
let messageStringID;
1024
if (
1025
aOldLogin.username === "" &&
1026
login.username !== "" &&
1027
login.password == aOldLogin.password
1028
) {
1029
// If the saved password matches the password we're prompting with then we
1030
// are only prompting to let the user add a username since there was one in
1031
// the form. Change the message so the purpose of the prompt is clearer.
1032
messageStringID = "updateLoginMsgAddUsername";
1033
}
1034
1035
let promptBrowser = LoginHelper.getBrowserForPrompt(aBrowser);
1036
LoginManagerPrompter._showLoginCaptureDoorhanger(
1037
promptBrowser,
1038
login,
1039
"password-change",
1040
{
1041
dismissed,
1042
extraAttr: notifySaved ? "attention" : "",
1043
},
1044
{
1045
notifySaved,
1046
messageStringID,
1047
autoSavedLoginGuid,
1048
}
1049
);
1050
1051
let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
1052
Services.obs.notifyObservers(
1053
aNewLogin,
1054
"passwordmgr-prompt-change",
1055
oldGUID
1056
);
1057
},
1058
1059
/**
1060
* Shows the Change Password dialog.
1061
*/
1062
_showChangeLoginDialog(aOldLogin, aNewLogin) {
1063
const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
1064
1065
var dialogText;
1066
if (aOldLogin.username) {
1067
dialogText = this._getLocalizedString("updatePasswordMsg", [
1068
aOldLogin.username,
1069
]);
1070
} else {
1071
dialogText = this._getLocalizedString("updatePasswordMsgNoUser");
1072
}
1073
1074
var dialogTitle = this._getLocalizedString("passwordChangeTitle");
1075
1076
// returns 0 for yes, 1 for no.
1077
var ok = !Services.prompt.confirmEx(
1078
this._chromeWindow,
1079
dialogTitle,
1080
dialogText,
1081
buttonFlags,
1082
null,
1083
null,
1084
null,
1085
null,
1086
{}
1087
);
1088
if (ok) {
1089
this.log("Updating password for user " + aOldLogin.username);
1090
this._updateLogin(aOldLogin, aNewLogin);
1091
}
1092
1093
let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
1094
Services.obs.notifyObservers(
1095
aNewLogin,
1096
"passwordmgr-prompt-change",
1097
oldGUID
1098
);
1099
},
1100
1101
/* ---------- Internal Methods ---------- */
1102
1103
_updateLogin(login, aNewLogin) {
1104
var now = Date.now();
1105
var propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
1106
Ci.nsIWritablePropertyBag
1107
);
1108
propBag.setProperty("formActionOrigin", aNewLogin.formActionOrigin);
1109
propBag.setProperty("origin", aNewLogin.origin);
1110
propBag.setProperty("password", aNewLogin.password);
1111
propBag.setProperty("username", aNewLogin.username);
1112
// Explicitly set the password change time here (even though it would
1113
// be changed automatically), to ensure that it's exactly the same
1114
// value as timeLastUsed.
1115
propBag.setProperty("timePasswordChanged", now);
1116
propBag.setProperty("timeLastUsed", now);
1117
propBag.setProperty("timesUsedIncrement", 1);
1118
Services.logins.modifyLogin(login, propBag);
1119
},
1120
1121
/**
1122
* Given a content DOM window, returns the chrome window and browser it's in.
1123
*/
1124
_getChromeWindow(aWindow) {
1125
// Handle non-e10s toolkit consumers.
1126
if (!Cu.isCrossProcessWrapper(aWindow)) {
1127
let browser = aWindow.docShell.chromeEventHandler;
1128
if (!browser) {
1129
return null;
1130
}
1131
1132
let chromeWin = browser.ownerGlobal;
1133
if (!chromeWin) {
1134
return null;
1135
}
1136
1137
return { win: chromeWin, browser };
1138
}
1139
1140
return null;
1141
},
1142
1143
_getNotifyWindow() {
1144
if (this._openerBrowser) {
1145
let chromeDoc = this._chromeWindow.document.documentElement;
1146
1147
// Check to see if the current window was opened with chrome
1148
// disabled, and if so use the opener window. But if the window
1149
// has been used to visit other pages (ie, has a history),
1150
// assume it'll stick around and *don't* use the opener.
1151
if (chromeDoc.getAttribute("chromehidden") && !this._browser.canGoBack) {
1152
this.log("Using opener window for notification prompt.");
1153
return {
1154
win: this._openerBrowser.ownerGlobal,
1155
browser: this._openerBrowser,
1156
};
1157
}
1158
}
1159
1160
return {
1161
win: this._chromeWindow,
1162
browser: this._browser,
1163
};
1164
},
1165
1166
/**
1167
* Returns the popup notification to this prompter,
1168
* or null if there isn't one available.
1169
*/
1170
_getPopupNote() {
1171
let popupNote = null;
1172
1173
try {
1174
let { win: notifyWin } = this._getNotifyWindow();
1175
1176
// .wrappedJSObject needed here -- see bug 422974 comment 5.
1177
popupNote = notifyWin.wrappedJSObject.PopupNotifications;
1178
} catch (e) {
1179
this.log("Popup notifications not available on window");
1180
}
1181
1182
return popupNote;
1183
},
1184
1185
/**
1186
* The user might enter a login that isn't the one we prefilled, but
1187
* is the same as some other existing login. So, pick a login with a
1188
* matching username, or return null.
1189
*/
1190
_repickSelectedLogin(foundLogins, username) {
1191
for (var i = 0; i < foundLogins.length; i++) {
1192
if (foundLogins[i].username == username) {
1193
return foundLogins[i];
1194
}
1195
}
1196
return null;
1197
},
1198
1199
/**
1200
* Can be called as:
1201
* _getLocalizedString("key1");
1202
* _getLocalizedString("key2", ["arg1"]);
1203
* _getLocalizedString("key3", ["arg1", "arg2"]);
1204
* (etc)
1205
*
1206
* Returns the localized string for the specified key,
1207
* formatted if required.
1208
*
1209
*/
1210
_getLocalizedString(key, formatArgs) {
1211
if (formatArgs) {
1212
return this._strBundle.formatStringFromName(key, formatArgs);
1213
}
1214
return this._strBundle.GetStringFromName(key);
1215
},
1216
1217
/**
1218
* Sanitizes the specified username, by stripping quotes and truncating if
1219
* it's too long. This helps prevent an evil site from messing with the
1220
* "save password?" prompt too much.
1221
*/
1222
_sanitizeUsername(username) {
1223
if (username.length > 30) {
1224
username = username.substring(0, 30);
1225
username += this._ellipsis;
1226
}
1227
return username.replace(/['"]/g, "");
1228
},
1229
1230
/**
1231
* The aURI parameter may either be a string uri, or an nsIURI instance.
1232
*
1233
* Returns the origin to use in a nsILoginInfo object (for example,
1234
* "http://example.com").
1235
*/
1236
_getFormattedOrigin(aURI) {
1237
let uri;
1238
if (aURI instanceof Ci.nsIURI) {
1239
uri = aURI;
1240
} else {
1241
uri = Services.io.newURI(aURI);
1242
}
1243
1244
return uri.scheme + "://" + uri.displayHostPort;
1245
},
1246
1247
/**
1248
* Converts a login's origin field (a URL) to a short string for
1249
* prompting purposes. Eg, "http://foo.com" --> "foo.com", or
1250
* "ftp://www.site.co.uk" --> "site.co.uk".
1251
*/
1252
_getShortDisplayHost(aURIString) {
1253
var displayHost;
1254
1255
var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
1256
Ci.nsIIDNService
1257
);
1258
try {
1259
var uri = Services.io.newURI(aURIString);
1260
var baseDomain = Services.eTLD.getBaseDomain(uri);
1261
displayHost = idnService.convertToDisplayIDN(baseDomain, {});
1262
} catch (e) {
1263
this.log("_getShortDisplayHost couldn't process " + aURIString);
1264
}
1265
1266
if (!displayHost) {
1267
displayHost = aURIString;
1268
}
1269
1270
return displayHost;
1271
},
1272
1273
/**
1274
* Returns the origin and realm for which authentication is being
1275
* requested, in the format expected to be used with nsILoginInfo.
1276
*/
1277
_getAuthTarget(aChannel, aAuthInfo) {
1278
var origin, realm;
1279
1280
// If our proxy is demanding authentication, don't use the
1281
// channel's actual destination.
1282
if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
1283
this.log("getAuthTarget is for proxy auth");
1284
if (!(aChannel instanceof Ci.nsIProxiedChannel)) {
1285
throw new Error("proxy auth needs nsIProxiedChannel");
1286
}
1287
1288
var info = aChannel.proxyInfo;
1289
if (!info) {
1290
throw new Error("proxy auth needs nsIProxyInfo");
1291
}
1292
1293
// Proxies don't have a scheme, but we'll use "moz-proxy://"
1294
// so that it's more obvious what the login is for.
1295
var idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
1296
Ci.nsIIDNService
1297
);
1298
origin =
1299
"moz-proxy://" +
1300
idnService.convertUTF8toACE(info.host) +
1301
":" +
1302
info.port;
1303
realm = aAuthInfo.realm;
1304
if (!realm) {
1305
realm = origin;
1306
}
1307
1308
return [origin, realm];
1309
}
1310
1311
origin = this._getFormattedOrigin(aChannel.URI);
1312
1313
// If a HTTP WWW-Authenticate header specified a realm, that value
1314
// will be available here. If it wasn't set or wasn't HTTP, we'll use
1315
// the formatted origin instead.
1316
realm = aAuthInfo.realm;
1317
if (!realm) {
1318
realm = origin;
1319
}
1320
1321
return [origin, realm];
1322
},
1323
1324
/**
1325
* Returns [username, password] as extracted from aAuthInfo (which
1326
* holds this info after having prompted the user).
1327
*
1328
* If the authentication was for a Windows domain, we'll prepend the
1329
* return username with the domain. (eg, "domain\user")
1330
*/
1331
_GetAuthInfo(aAuthInfo) {
1332
var username, password;
1333
1334
var flags = aAuthInfo.flags;
1335
if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain) {
1336
username = aAuthInfo.domain + "\\" + aAuthInfo.username;
1337
} else {
1338
username = aAuthInfo.username;
1339
}
1340
1341
password = aAuthInfo.password;
1342
1343
return [username, password];
1344
},
1345
1346
/**
1347
* Given a username (possibly in DOMAIN\user form) and password, parses the
1348
* domain out of the username if necessary and sets domain, username and
1349
* password on the auth information object.
1350
*/
1351
_SetAuthInfo(aAuthInfo, username, password) {
1352
var flags = aAuthInfo.flags;
1353
if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
1354
// Domain is separated from username by a backslash
1355
var idx = username.indexOf("\\");
1356
if (idx == -1) {
1357
aAuthInfo.username = username;
1358
} else {
1359
aAuthInfo.domain = username.substring(0, idx);
1360
aAuthInfo.username = username.substring(idx + 1);
1361
}
1362
} else {
1363
aAuthInfo.username = username;
1364
}
1365
aAuthInfo.password = password;
1366
},
1367
1368
_newAsyncPromptConsumer(aCallback, aContext) {
1369
return {
1370
QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
1371
callback: aCallback,
1372
context: aContext,
1373
cancel() {
1374
this.callback.onAuthCancelled(this.context, false);
1375
this.callback = null;
1376
this.context = null;
1377
},
1378
};
1379
},
1380
}; // end of LoginManagerAuthPrompter implementation
1381
1382
XPCOMUtils.defineLazyGetter(LoginManagerAuthPrompter.prototype, "log", () => {
1383
let logger = LoginHelper.createLogger("LoginManagerAuthPrompter");
1384
return logger.log.bind(logger);
1385
});
1386
1387
const EXPORTED_SYMBOLS = [
1388
"LoginManagerAuthPromptFactory",
1389
"LoginManagerAuthPrompter",
1390
];