Bug 475053 - Implement asyncPromptAuth to fix multiple password prompt overlap, r=bzbarsky+dolske

This commit is contained in:
Honza Bambas 2009-07-20 20:29:41 +02:00
parent 8c0c5a9a38
commit 66ab2c6182
10 changed files with 1021 additions and 62 deletions

View File

@ -48,7 +48,7 @@ interface nsIAuthInformation;
* It can be used to prompt users for authentication information, either
* synchronously or asynchronously.
*/
[scriptable, uuid(447fc780-1d28-412a-91a1-466d48129c65)]
[scriptable, uuid(651395EB-8612-4876-8AC0-A88D4DCE9E1E)]
interface nsIAuthPrompt2 : nsISupports
{
/** @name Security Levels */
@ -110,9 +110,23 @@ interface nsIAuthPrompt2 : nsISupports
* dialog and MUST call nsIAuthPromptCallback::onAuthCancelled on the provided
* callback.
*
* @throw NS_ERROR_NOT_IMPLEMENTED
* Asynchronous authentication prompts are not supported;
* the caller should fall back to promptUsernameAndPassword().
* This implementation may:
*
* 1) Coalesce identical prompts. This means prompts that are guaranteed to
* want the same auth information from the user. A single prompt will be
* shown; then the callbacks for all the coalesced prompts will be notified
* with the resulting auth information.
* 2) Serialize prompts that are all in the same "context" (this might mean
* application-wide, for a given window, or something else depending on
* the user interface) so that the user is not deluged with prompts.
*
* @throw
* This method may throw any exception when the prompt fails to queue e.g
* because of out-of-memory error. It must not throw when the prompt
* could already be potentially shown to the user. In that case information
* about the failure has to come through the callback. This way we
* prevent multiple dialogs shown to the user because consumer may fall
* back to synchronous prompt on synchronous failure of this method.
*/
nsICancelable asyncPromptAuth(in nsIChannel aChannel,
in nsIAuthPromptCallback aCallback,
@ -120,5 +134,3 @@ interface nsIAuthPrompt2 : nsISupports
in PRUint32 level,
in nsIAuthInformation authInfo);
};

View File

@ -83,6 +83,7 @@
#include "nsAuthInformationHolder.h"
#include "nsICacheService.h"
#include "nsDNSPrefetch.h"
#include "nsNetSegmentUtils.h"
// True if the local cache should be bypassed when processing a request.
#define BYPASS_LOCAL_CACHE(loadFlags) \
@ -125,6 +126,7 @@ nsHttpChannel::nsHttpChannel()
, mTransactionReplaced(PR_FALSE)
, mUploadStreamHasHeaders(PR_FALSE)
, mAuthRetryPending(PR_FALSE)
, mProxyAuth(PR_FALSE)
, mSuppressDefensiveAuth(PR_FALSE)
, mResuming(PR_FALSE)
, mInitedCacheEntry(PR_FALSE)
@ -3117,13 +3119,13 @@ nsHttpChannel::ProcessAuthentication(PRUint32 httpStatus)
}
const char *challenges;
PRBool proxyAuth = (httpStatus == 407);
mProxyAuth = (httpStatus == 407);
nsresult rv = PrepareForAuthentication(proxyAuth);
nsresult rv = PrepareForAuthentication(mProxyAuth);
if (NS_FAILED(rv))
return rv;
if (proxyAuth) {
if (mProxyAuth) {
// only allow a proxy challenge if we have a proxy server configured.
// otherwise, we could inadvertantly expose the user's proxy
// credentials to an origin server. We could attempt to proceed as
@ -3148,12 +3150,24 @@ nsHttpChannel::ProcessAuthentication(PRUint32 httpStatus)
NS_ENSURE_TRUE(challenges, NS_ERROR_UNEXPECTED);
nsCAutoString creds;
rv = GetCredentials(challenges, proxyAuth, creds);
if (NS_FAILED(rv))
rv = GetCredentials(challenges, mProxyAuth, creds);
if (rv == NS_ERROR_IN_PROGRESS) {
// authentication prompt has been invoked and result
// is expected asynchronously
mAuthRetryPending = PR_TRUE;
// suspend the transaction pump to stop receiving the
// unauthenticated content data. We will throw that data
// away when user provides credentials or resume the pump
// when user refuses to authenticate.
LOG(("Suspending the transaction, asynchronously prompting for credentials"));
mTransactionPump->Suspend();
return NS_OK;
}
else if (NS_FAILED(rv))
LOG(("unable to authenticate\n"));
else {
// set the authentication credentials
if (proxyAuth)
if (mProxyAuth)
mRequestHead.SetHeader(nsHttp::Proxy_Authorization, creds);
else
mRequestHead.SetHeader(nsHttp::Authorization, creds);
@ -3280,6 +3294,15 @@ nsHttpChannel::GetCredentials(const char *challenges,
break;
}
else if (rv == NS_ERROR_IN_PROGRESS) {
// authentication prompt has been invoked and result is
// expected asynchronously, save current challenge being
// processed and all remaining challenges to use later in
// OnAuthAvailable and now immediately return
mCurrentChallenge = challenge;
mRemainingChallenges = eol ? eol+1 : nsnull;
return rv;
}
// reset the auth type and continuation state
NS_IF_RELEASE(*currentContinuationState);
@ -3299,6 +3322,43 @@ nsHttpChannel::GetCredentials(const char *challenges,
return rv;
}
nsresult
nsHttpChannel::GetAuthorizationMembers(PRBool proxyAuth,
nsCSubstring& scheme,
const char*& host,
PRInt32& port,
nsCSubstring& path,
nsHttpAuthIdentity*& ident,
nsISupports**& continuationState)
{
if (proxyAuth) {
NS_ASSERTION (mConnectionInfo->UsingHttpProxy(), "proxyAuth is true, but no HTTP proxy is configured!");
host = mConnectionInfo->ProxyHost();
port = mConnectionInfo->ProxyPort();
ident = &mProxyIdent;
scheme.AssignLiteral("http");
continuationState = &mProxyAuthContinuationState;
}
else {
host = mConnectionInfo->Host();
port = mConnectionInfo->Port();
ident = &mIdent;
nsresult rv;
rv = GetCurrentPath(path);
if (NS_FAILED(rv)) return rv;
rv = mURI->GetScheme(scheme);
if (NS_FAILED(rv)) return rv;
continuationState = &mAuthContinuationState;
}
return NS_OK;
}
nsresult
nsHttpChannel::GetCredentialsForChallenge(const char *challenge,
const char *authType,
@ -3340,35 +3400,16 @@ nsHttpChannel::GetCredentialsForChallenge(const char *challenge,
PRBool identFromURI = PR_FALSE;
nsISupports **continuationState;
if (proxyAuth) {
NS_ASSERTION (mConnectionInfo->UsingHttpProxy(), "proxyAuth is true, but no HTTP proxy is configured!");
host = mConnectionInfo->ProxyHost();
port = mConnectionInfo->ProxyPort();
ident = &mProxyIdent;
scheme.AssignLiteral("http");
continuationState = &mProxyAuthContinuationState;
}
else {
host = mConnectionInfo->Host();
port = mConnectionInfo->Port();
ident = &mIdent;
rv = GetCurrentPath(path);
if (NS_FAILED(rv)) return rv;
rv = mURI->GetScheme(scheme);
if (NS_FAILED(rv)) return rv;
rv = GetAuthorizationMembers(proxyAuth, scheme, host, port, path, ident, continuationState);
if (NS_FAILED(rv)) return rv;
if (!proxyAuth) {
// if this is the first challenge, then try using the identity
// specified in the URL.
if (mIdent.IsEmpty()) {
GetIdentityFromURI(authFlags, mIdent);
identFromURI = !mIdent.IsEmpty();
}
continuationState = &mAuthContinuationState;
}
//
@ -3622,25 +3663,165 @@ nsHttpChannel::PromptForIdentity(PRUint32 level,
nsDependentCString(authType));
if (!holder)
return NS_ERROR_OUT_OF_MEMORY;
PRBool retval = PR_FALSE;
rv = authPrompt->PromptAuth(this,
level,
holder, &retval);
if (NS_FAILED(rv))
return rv;
rv = authPrompt->AsyncPromptAuth(this, this, nsnull, level, holder,
getter_AddRefs(mAsyncPromptAuthCancelable));
if (NS_SUCCEEDED(rv)) {
// indicate using this error code that authentication prompt
// result is expected asynchronously
rv = NS_ERROR_IN_PROGRESS;
}
else {
// Fall back to synchronous prompt
PRBool retval = PR_FALSE;
rv = authPrompt->PromptAuth(this, level, holder, &retval);
if (NS_FAILED(rv))
return rv;
if (!retval)
rv = NS_ERROR_ABORT;
else
holder->SetToHttpAuthIdentity(authFlags, ident);
}
// remember that we successfully showed the user an auth dialog
if (!proxyAuth)
mSuppressDefensiveAuth = PR_TRUE;
if (!retval)
rv = NS_ERROR_ABORT;
else
holder->SetToHttpAuthIdentity(authFlags, ident);
return rv;
}
NS_IMETHODIMP nsHttpChannel::OnAuthAvailable(nsISupports *aContext,
nsIAuthInformation *aAuthInfo)
{
LOG(("nsHttpChannel::OnAuthAvailable [this=%x]", this));
mAsyncPromptAuthCancelable = nsnull;
nsresult rv;
const char *host;
PRInt32 port;
nsHttpAuthIdentity *ident;
nsCAutoString path, scheme;
nsISupports **continuationState;
rv = GetAuthorizationMembers(mProxyAuth, scheme, host, port, path, ident, continuationState);
if (NS_FAILED(rv))
OnAuthCancelled(aContext, PR_FALSE);
nsCAutoString realm;
ParseRealm(mCurrentChallenge.get(), realm);
nsHttpAuthCache *authCache = gHttpHandler->AuthCache();
nsHttpAuthEntry *entry = nsnull;
authCache->GetAuthEntryForDomain(scheme.get(), host, port, realm.get(), &entry);
nsCOMPtr<nsISupports> sessionStateGrip;
if (entry)
sessionStateGrip = entry->mMetaData;
nsAuthInformationHolder* holder =
static_cast<nsAuthInformationHolder*>(aAuthInfo);
ident->Set(holder->Domain().get(),
holder->User().get(),
holder->Password().get());
nsCAutoString unused;
nsCOMPtr<nsIHttpAuthenticator> auth;
rv = GetAuthenticator(mCurrentChallenge.get(), unused, getter_AddRefs(auth));
if (NS_FAILED(rv)) {
NS_ASSERTION(PR_FALSE, "GetAuthenticator failed");
OnAuthCancelled(aContext, PR_TRUE);
return NS_OK;
}
nsXPIDLCString creds;
rv = GenCredsAndSetEntry(auth, mProxyAuth,
scheme.get(), host, port, path.get(),
realm.get(), mCurrentChallenge.get(), *ident, sessionStateGrip,
getter_Copies(creds));
mCurrentChallenge.Truncate();
if (NS_FAILED(rv)) {
OnAuthCancelled(aContext, PR_TRUE);
return NS_OK;
}
return ContinueOnAuthAvailable(creds);
}
NS_IMETHODIMP nsHttpChannel::OnAuthCancelled(nsISupports *aContext,
PRBool userCancel)
{
LOG(("nsHttpChannel::OnAuthCancelled [this=%x]", this));
mAsyncPromptAuthCancelable = nsnull;
if (userCancel) {
if (!mRemainingChallenges.IsEmpty()) {
// there are still some challenges to process, do so
nsresult rv;
nsCAutoString creds;
rv = GetCredentials(mRemainingChallenges.get(), mProxyAuth, creds);
if (NS_SUCCEEDED(rv)) {
// GetCredentials loaded the credentials from the cache or
// some other way in a synchronous manner, process those
// credentials now
mRemainingChallenges.Truncate();
return ContinueOnAuthAvailable(creds);
}
else if (rv == NS_ERROR_IN_PROGRESS) {
// GetCredentials successfully queued another authprompt for
// a challenge from the list, we are now waiting for the user
// to provide the credentials
return NS_OK;
}
// otherwise, we failed...
}
mRemainingChallenges.Truncate();
// ensure call of OnStartRequest of the current listener here,
// it would not be called otherwise at all
nsresult rv = CallOnStartRequest();
// drop mAuthRetryPending flag and resume the transaction
// this resumes load of the unauthenticated content data
mAuthRetryPending = PR_FALSE;
LOG(("Resuming the transaction, user cancelled the auth dialog"));
mTransactionPump->Resume();
if (NS_FAILED(rv))
mTransactionPump->Cancel(rv);
}
return NS_OK;
}
nsresult
nsHttpChannel::ContinueOnAuthAvailable(const nsCSubstring& creds)
{
if (mProxyAuth)
mRequestHead.SetHeader(nsHttp::Proxy_Authorization, creds);
else
mRequestHead.SetHeader(nsHttp::Authorization, creds);
// drop our remaining list of challenges. We don't need them, because we
// have now authenticated against a challenge and will be sending that
// information to the server (or proxy). If it doesn't accept our
// authentication it'll respond with failure and resend the challenge list
mRemainingChallenges.Truncate();
// setting mAuthRetryPending flag and resuming the transaction
// triggers process of throwing away the unauthenticated data already
// coming from the network
mAuthRetryPending = PR_TRUE;
LOG(("Resuming the transaction, we got credentials from user"));
mTransactionPump->Resume();
return NS_OK;
}
PRBool
nsHttpChannel::ConfirmAuth(const nsString &bundleKey, PRBool doYesNoPrompt)
{
@ -3896,6 +4077,7 @@ NS_INTERFACE_MAP_BEGIN(nsHttpChannel)
NS_INTERFACE_MAP_ENTRY(nsITraceableChannel)
NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheContainer)
NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheChannel)
NS_INTERFACE_MAP_ENTRY(nsIAuthPromptCallback)
NS_INTERFACE_MAP_END_INHERITING(nsHashPropertyBag)
//-----------------------------------------------------------------------------
@ -3943,6 +4125,8 @@ nsHttpChannel::Cancel(nsresult status)
mTransactionPump->Cancel(status);
if (mCachePump)
mCachePump->Cancel(status);
if (mAsyncPromptAuthCancelable)
mAsyncPromptAuthCancelable->Cancel(status);
return NS_OK;
}
@ -4973,8 +5157,14 @@ nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt, nsresult st
// keep the connection around after the transaction is finished.
//
nsRefPtr<nsAHttpConnection> conn;
if (authRetry && (mCaps & NS_HTTP_STICKY_CONNECTION))
if (authRetry && (mCaps & NS_HTTP_STICKY_CONNECTION)) {
conn = mTransaction->Connection();
// This is so far a workaround to fix leak when reusing unpersistent
// connection for authentication retry. See bug 459620 comment 4
// for details.
if (conn && !conn->IsPersistent())
conn = nsnull;
}
// at this point, we're done with the transaction
NS_RELEASE(mTransaction);

View File

@ -84,6 +84,7 @@
#include "nsICancelable.h"
#include "nsIProxiedChannel.h"
#include "nsITraceableChannel.h"
#include "nsIAuthPromptCallback.h"
class nsHttpResponseHead;
class nsAHttpConnection;
@ -109,6 +110,7 @@ class nsHttpChannel : public nsHashPropertyBag
, public nsIProxiedChannel
, public nsITraceableChannel
, public nsIApplicationCacheChannel
, public nsIAuthPromptCallback
{
public:
NS_DECL_ISUPPORTS_INHERITED
@ -130,6 +132,7 @@ public:
NS_DECL_NSITRACEABLECHANNEL
NS_DECL_NSIAPPLICATIONCACHECONTAINER
NS_DECL_NSIAPPLICATIONCACHECHANNEL
NS_DECL_NSIAUTHPROMPTCALLBACK
nsHttpChannel();
virtual ~nsHttpChannel();
@ -223,19 +226,38 @@ private:
// auth specific methods
nsresult PrepareForAuthentication(PRBool proxyAuth);
nsresult GenCredsAndSetEntry(nsIHttpAuthenticator *, PRBool proxyAuth, const char *scheme, const char *host, PRInt32 port, const char *dir, const char *realm, const char *challenge, const nsHttpAuthIdentity &ident, nsCOMPtr<nsISupports> &session, char **result);
nsresult GetCredentials(const char *challenges, PRBool proxyAuth, nsAFlatCString &creds);
nsresult GetCredentialsForChallenge(const char *challenge, const char *scheme, PRBool proxyAuth, nsIHttpAuthenticator *auth, nsAFlatCString &creds);
nsresult GetAuthenticator(const char *challenge, nsCString &scheme, nsIHttpAuthenticator **auth);
void ParseRealm(const char *challenge, nsACString &realm);
void GetIdentityFromURI(PRUint32 authFlags, nsHttpAuthIdentity&);
/**
* Following three methods return NS_ERROR_IN_PROGRESS when
* nsIAuthPrompt2.asyncPromptAuth method is called. This result indicates
* the user's decision will be gathered in a callback and is not an actual
* error.
*/
nsresult GetCredentials(const char *challenges, PRBool proxyAuth, nsAFlatCString &creds);
nsresult GetCredentialsForChallenge(const char *challenge, const char *scheme, PRBool proxyAuth, nsIHttpAuthenticator *auth, nsAFlatCString &creds);
nsresult PromptForIdentity(PRUint32 level, PRBool proxyAuth, const char *realm, const char *authType, PRUint32 authFlags, nsHttpAuthIdentity &);
PRBool ConfirmAuth(const nsString &bundleKey, PRBool doYesNoPrompt);
void CheckForSuperfluousAuth();
void SetAuthorizationHeader(nsHttpAuthCache *, nsHttpAtom header, const char *scheme, const char *host, PRInt32 port, const char *path, nsHttpAuthIdentity &ident);
void AddAuthorizationHeaders();
nsresult GetCurrentPath(nsACString &);
/**
* Return all information needed to build authorization information,
* all paramters except proxyAuth are out parameters. proxyAuth specifies
* with what authorization we work (WWW or proxy).
*/
nsresult GetAuthorizationMembers(PRBool proxyAuth, nsCSubstring& scheme, const char*& host, PRInt32& port, nsCSubstring& path, nsHttpAuthIdentity*& ident, nsISupports**& continuationState);
nsresult DoAuthRetry(nsAHttpConnection *);
PRBool MustValidateBasedOnQueryUrl();
/**
* Method called to resume suspended transaction after we got credentials
* from the user. Called from OnAuthAvailable callback or OnAuthCancelled
* when credentials for next challenge were obtained synchronously.
*/
nsresult ContinueOnAuthAvailable(const nsCSubstring& creds);
private:
nsCOMPtr<nsIURI> mOriginalURI;
@ -293,6 +315,19 @@ private:
nsHttpAuthIdentity mIdent;
nsHttpAuthIdentity mProxyIdent;
// Reference to the prompt wating in prompt queue. The channel is
// responsible to call its cancel method when user in any way cancels
// this request.
nsCOMPtr<nsICancelable> mAsyncPromptAuthCancelable;
// Saved in GetCredentials when prompt is asynchronous, the first challenge
// we obtained from the server with 401/407 response, will be processed in
// OnAuthAvailable callback.
nsCString mCurrentChallenge;
// Saved in GetCredentials when prompt is asynchronous, remaning challenges
// we have to process when user cancels the auth dialog for the current
// challenge.
nsCString mRemainingChallenges;
// Resumable channel specific data
nsCString mEntityID;
PRUint64 mStartPos;
@ -329,6 +364,9 @@ private:
PRUint32 mTransactionReplaced : 1;
PRUint32 mUploadStreamHasHeaders : 1;
PRUint32 mAuthRetryPending : 1;
// True when we need to authenticate to proxy, i.e. when we get 407
// response. Used in OnAuthAvailable and OnAuthCancelled callbacks.
PRUint32 mProxyAuth : 1;
PRUint32 mSuppressDefensiveAuth : 1;
PRUint32 mResuming : 1;
PRUint32 mInitedCacheEntry : 1;

View File

@ -127,7 +127,7 @@ AuthPrompt2.prototype = {
},
asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
do_throw("not implemented yet")
throw 0x80004001;
}
};
@ -195,7 +195,7 @@ RealmTestRequestor.prototype = {
},
asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) {
do_throw("not implemented yet");
throw 0x80004001;
}
};

View File

@ -38,7 +38,7 @@ AuthPrompt2.prototype = {
},
asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
do_throw("not implemented yet")
throw 0x80004001;
}
};

View File

@ -50,18 +50,42 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
* Invoked by NS_NewAuthPrompter2()
* [embedding/components/windowwatcher/src/nsPrompt.cpp]
*/
function LoginManagerPromptFactory() {}
function LoginManagerPromptFactory() {
var observerService = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
observerService.addObserver(this, "quit-application-granted", true);
}
LoginManagerPromptFactory.prototype = {
classDescription : "LoginManagerPromptFactory",
contractID : "@mozilla.org/passwordmanager/authpromptfactory;1",
classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory]),
QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]),
_asyncPrompts : {},
_asyncPromptInProgress : false,
observe : function (subject, topic, data) {
if (topic == "quit-application-granted") {
var asyncPrompts = this._asyncPrompts;
this._asyncPrompts = {};
for each (var asyncPrompt in asyncPrompts) {
for each (var consumer in asyncPrompt.consumers) {
if (consumer.callback) {
this.log("Canceling async auth prompt callback " + consumer.callback);
try {
consumer.callback.onAuthCancelled(consumer.context, true);
} catch (e) { /* Just ignore exceptions from the callback */ }
}
}
}
}
},
getPrompt : function (aWindow, aIID) {
var prompt = new LoginManagerPrompter().QueryInterface(aIID);
prompt.init(aWindow);
prompt.init(aWindow, this);
return prompt;
}
}; // end of LoginManagerPromptFactory implementation
@ -98,6 +122,7 @@ LoginManagerPrompter.prototype = {
Ci.nsIAuthPrompt2,
Ci.nsILoginManagerPrompter]),
_factory : null,
_window : null,
_debug : false, // mirrors signon.debug
@ -165,6 +190,14 @@ LoginManagerPrompter.prototype = {
return this.__ioService;
},
__threadManager: null,
get _threadManager() {
if (!this.__threadManager)
this.__threadManager = Cc["@mozilla.org/thread-manager;1"].
getService(Ci.nsIThreadManager);
return this.__threadManager;
},
__ellipsis : null,
get _ellipsis() {
@ -556,10 +589,114 @@ LoginManagerPrompter.prototype = {
return ok;
},
asyncPromptAuth : function () {
return NS_ERROR_NOT_IMPLEMENTED;
asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) {
var cancelable = null;
try {
this.log("===== asyncPromptAuth called =====");
// If the user submits a login but it fails, we need to remove the
// notification bar that was displayed. Conveniently, the user will
// be prompted for authentication again, which brings us here.
var notifyBox = this._getNotifyBox();
if (notifyBox)
this._removeLoginNotifications(notifyBox);
cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
var hashKey = aLevel + "|" + hostname + "|" + httpRealm;
this.log("Async prompt key = " + hashKey);
var asyncPrompt = this._factory._asyncPrompts[hashKey];
if (asyncPrompt) {
this.log("Prompt bound to an existing one in the queue, callback = " + aCallback);
asyncPrompt.consumers.push(cancelable);
return cancelable;
}
this.log("Adding new prompt to the queue, callback = " + aCallback);
asyncPrompt = {
consumers: [cancelable],
channel: aChannel,
authInfo: aAuthInfo,
level: aLevel
}
this._factory._asyncPrompts[hashKey] = asyncPrompt;
this._doAsyncPrompt();
}
catch (e) {
Components.utils.reportError("LoginManagerPrompter: " +
"asyncPromptAuth: " + e + "\nFalling back to promptAuth\n");
// Fail the prompt operation to let the consumer fall back
// to synchronous promptAuth method
throw e;
}
return cancelable;
},
_doAsyncPrompt : function() {
if (this._factory._asyncPromptInProgress) {
this.log("_doAsyncPrompt bypassed, already in progress");
return;
}
// Find the first prompt key we have in the queue
var hashKey = null;
for (hashKey in this._factory._asyncPrompts)
break;
if (!hashKey) {
this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
return;
}
this._factory._asyncPromptInProgress = true;
var self = this;
var runnable = {
run : function() {
var ok = false;
var prompt = self._factory._asyncPrompts[hashKey];
try {
self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'");
ok = self.promptAuth(
prompt.channel,
prompt.level,
prompt.authInfo
);
} catch (e) {
Components.utils.reportError("LoginManagerPrompter: " +
"_doAsyncPrompt:run: " + e + "\n");
}
delete self._factory._asyncPrompts[hashKey];
self._factory._asyncPromptInProgress = false;
for each (var consumer in prompt.consumers) {
if (!consumer.callback)
// Not having a callback means that consumer didn't provide it
// or canceled the notification
continue;
self.log("Calling back to " + consumer.callback + " ok=" + ok);
try {
if (ok)
consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
else
consumer.callback.onAuthCancelled(consumer.context, true);
} catch (e) { /* Throw away exceptions caused by callback */ }
}
self._doAsyncPrompt();
}
}
this._threadManager.mainThread.dispatch(runnable,
Ci.nsIThread.DISPATCH_NORMAL);
this.log("_doAsyncPrompt:run dispatched");
},
@ -572,8 +709,9 @@ LoginManagerPrompter.prototype = {
* init
*
*/
init : function (aWindow) {
init : function (aWindow, aFactory) {
this._window = aWindow;
this._factory = aFactory || null;
var prefBranch = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefService).getBranch("signon.");
@ -1218,6 +1356,19 @@ LoginManagerPrompter.prototype = {
aAuthInfo.username = username;
}
aAuthInfo.password = password;
},
_newAsyncPromptConsumer : function(aCallback, aContext) {
return {
QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
callback: aCallback,
context: aContext,
cancel: function() {
this.callback.onAuthCancelled(this.context, false);
this.callback = null;
this.context = null;
}
}
}
}; // end of LoginManagerPrompter implementation

View File

@ -74,6 +74,7 @@ MOCHI_TESTS = \
test_bug_391514.html \
test_bug_427033.html \
test_bug_444968.html \
test_prompt_async.html \
test_notifications.html \
test_prompt.html \
test_xhr.html \
@ -101,6 +102,7 @@ MOCHI_CONTENT = \
subtst_notifications_8.html \
subtst_notifications_9.html \
subtst_notifications_10.html \
subtst_prompt_async.html \
$(NULL)
XPCSHELL_TESTS = unit

View File

@ -11,13 +11,16 @@ function handleRequest(request, response)
function reallyHandleRequest(request, response) {
var match;
var requestAuth = true;
var requestAuth = true, requestProxyAuth = true;
// Allow the caller to drive how authentication is processed via the query.
// Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
var query = request.queryString;
var expected_user = "", expected_pass = "", realm = "mochitest";
var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
var huge = false;
var authHeaderCount = 1;
// user=xxx
match = /user=([^&]*)/.exec(query);
if (match)
@ -33,6 +36,31 @@ function reallyHandleRequest(request, response) {
if (match)
realm = match[1];
// proxy_user=xxx
match = /proxy_user=([^&]*)/.exec(query);
if (match)
proxy_expected_user = match[1];
// proxy_pass=xxx
match = /proxy_pass=([^&]*)/.exec(query);
if (match)
proxy_expected_pass = match[1];
// proxy_realm=xxx
match = /proxy_realm=([^&]*)/.exec(query);
if (match)
proxy_realm = match[1];
// huge=1
match = /huge=1/.exec(query);
if (match)
huge = true;
// multiple=1
match = /multiple=([^&]*)/.exec(query);
if (match)
authHeaderCount = match[1]+0;
// Look for an authentication header, if any, in the request.
//
@ -56,16 +84,40 @@ function reallyHandleRequest(request, response) {
actual_pass = match[2];
}
var proxy_actual_user = "", proxy_actual_pass = "";
if (request.hasHeader("Proxy-Authorization")) {
authHeader = request.getHeader("Proxy-Authorization");
match = /Basic (.+)/.exec(authHeader);
if (match.length != 2)
throw "Couldn't parse auth header: " + authHeader;
var userpass = base64ToString(match[1]); // no atob() :-(
match = /(.*):(.*)/.exec(userpass);
if (match.length != 3)
throw "Couldn't decode auth header: " + userpass;
proxy_actual_user = match[1];
proxy_actual_pass = match[2];
}
// Don't request authentication if the credentials we got were what we
// expected.
if (expected_user == actual_user &&
expected_pass == actual_pass) {
requestAuth = false;
expected_pass == actual_pass) {
requestAuth = false;
}
if (proxy_expected_user == proxy_actual_user &&
proxy_expected_pass == proxy_actual_pass) {
requestProxyAuth = false;
}
if (requestAuth) {
if (requestProxyAuth) {
response.setStatusLine("1.0", 407, "Proxy authentication required");
for (i = 0; i < authHeaderCount; ++i)
response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
} else if (requestAuth) {
response.setStatusLine("1.0", 401, "Authentication required");
response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", false);
for (i = 0; i < authHeaderCount; ++i)
response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
} else {
response.setStatusLine("1.0", 200, "OK");
}
@ -73,9 +125,20 @@ function reallyHandleRequest(request, response) {
response.setHeader("Content-Type", "application/xhtml+xml", false);
response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
if (huge) {
response.write("<div style='display: none'>");
for (i = 0; i < 100000; i++) {
response.write("123456789\n");
}
response.write("</div>");
response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
}
response.write("</html>");
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Multiple auth request</title>
</head>
<body>
<iframe id="iframe1" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=1&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
<iframe id="iframe2" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=2&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
<iframe id="iframe3" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=3&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
</body>
</html>

View File

@ -0,0 +1,492 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for Async Auth Prompt</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="prompt_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
// Class monitoring number of open dialog windows
// It checks there is always open just a single dialog per application
function dialogMonitor() {
var observerService = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
observerService.addObserver(this, "domwindowopened", false);
observerService.addObserver(this, "domwindowclosed", false);
}
dialogMonitor.prototype = {
windowsOpen : new Array(),
windowsRegistered : 0,
QueryInterface : function (iid) {
const interfaces = [Ci.nsIObserver,
Ci.nsISupports];
if (!interfaces.some( function(v) { return iid.equals(v) } ))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
},
observe: function(subject, topic, data) {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
if (topic === "domwindowopened") {
var win = subject.QueryInterface(Ci.nsIDOMWindow);
this.windowsOpen.push(win);
ok(this.windowsOpen.length == 1, "Didn't open more then one dialog at a time");
this.windowsRegistered++;
return;
}
if (topic === "domwindowclosed") {
var win = subject.QueryInterface(Ci.nsIDOMWindow);
for (p in this.windowsOpen)
if (win == this.windowsOpen[p]) {
this.windowsOpen.splice(p, 1);
break;
}
return;
}
},
shutdown: function() {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
var observerService = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
observerService.removeObserver(this, "domwindowopened");
observerService.removeObserver(this, "domwindowclosed");
},
reset: function() {
while (this.windowsOpen.length)
this.windowsOpen.shift();
this.windowsRegistered = 0;
}
}
var monitor = new dialogMonitor();
var pwmgr, logins = [];
function initLogins() {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
pwmgr = Cc["@mozilla.org/login-manager;1"].
getService(Ci.nsILoginManager);
function addLogin(host, realm, user, pass) {
var login = Cc["@mozilla.org/login-manager/loginInfo;1"].
createInstance(Ci.nsILoginInfo);
login.init(host, null, realm, user, pass, "", "");
pwmgr.addLogin(login);
logins.push(login);
}
addLogin("moz-proxy://127.0.0.1:8888", "proxy_realm",
"proxy_user", "proxy_pass");
addLogin("moz-proxy://127.0.0.1:8888", "proxy_realm2",
"proxy_user2", "proxy_pass2");
addLogin("moz-proxy://127.0.0.1:8888", "proxy_realm3",
"proxy_user3", "proxy_pass3");
addLogin("moz-proxy://127.0.0.1:8888", "proxy_realm4",
"proxy_user4", "proxy_pass4");
addLogin("moz-proxy://127.0.0.1:8888", "proxy_realm5",
"proxy_user5", "proxy_pass5");
addLogin("http://example.com", "mochirealm",
"user1name", "user1pass");
addLogin("http://example.org", "mochirealm2",
"user2name", "user2pass");
addLogin("http://example.com", "mochirealm3",
"user3name", "user3pass");
addLogin("http://example.com", "mochirealm4",
"user4name", "user4pass");
addLogin("http://example.com", "mochirealm5",
"user5name", "user5pass");
addLogin("http://example.com", "mochirealm6",
"user6name", "user6pass");
}
function finishTest() {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
ok(true, "finishTest removing testing logins...");
for (i in logins)
pwmgr.removeLogin(logins[i]);
var authMgr = Cc['@mozilla.org/network/http-auth-manager;1'].
getService(Ci.nsIHttpAuthManager);
authMgr.clearAll();
monitor.shutdown();
SimpleTest.finish();
}
// --------------- Test loop spin ----------------
var testNum = 1;
var iframe1;
var iframe2a;
var iframe2b;
window.onload = function () {
iframe1 = document.getElementById("iframe1");
iframe2a = document.getElementById("iframe2a");
iframe2b = document.getElementById("iframe2b");
iframe1.onload = onFrameLoad;
iframe2a.onload = onFrameLoad;
iframe2b.onload = onFrameLoad;
initLogins();
doTest(testNum);
}
var expectedLoads;
var expectedDialogs;
function onFrameLoad()
{
if (--expectedLoads == 0) {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
// All pages expected to load has loaded, continue with the next test
ok(true, "Expected frames loaded");
doCheck(testNum);
monitor.reset();
testNum++;
doTest(testNum);
}
}
function doTest(testNum)
{
var exampleCom = "http://example.com/tests/toolkit/components/passwordmgr/test/";
var exampleOrg = "http://example.org/tests/toolkit/components/passwordmgr/test/";
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
switch (testNum)
{
case 1:
// Load through a single proxy with authentication required 3 different
// pages, first with one login, other two with their own different login.
// We expect to show just a single dialog for proxy authentication and
// then two dialogs to authenticate to login 1 and then login 2.
ok(true, "doTest testNum 1");
expectedLoads = 3;
expectedDialogs = 3;
iframe1.src = exampleCom + "authenticate.sjs?"+
"r=1&"+
"user=user1name&"+
"pass=user1pass&"+
"realm=mochirealm&"+
"proxy_user=proxy_user&"+
"proxy_pass=proxy_pass&"+
"proxy_realm=proxy_realm";
iframe2a.src = exampleOrg + "authenticate.sjs?"+
"r=2&"+
"user=user2name&"+
"pass=user2pass&"+
"realm=mochirealm2&"+
"proxy_user=proxy_user&"+
"proxy_pass=proxy_pass&"+
"proxy_realm=proxy_realm";
iframe2b.src = exampleOrg + "authenticate.sjs?"+
"r=3&"+
"user=user2name&"+
"pass=user2pass&"+
"realm=mochirealm2&"+
"proxy_user=proxy_user&"+
"proxy_pass=proxy_pass&"+
"proxy_realm=proxy_realm";
break;
case 2:
// Load an iframe with 3 subpages all requiring the same login through
// anuthenticated proxy. We expect 2 dialogs, proxy authentication
// and web authentication.
ok(true, "doTest testNum 2");
expectedLoads = 3;
expectedDialogs = 2;
iframe1.src = exampleCom + "subtst_prompt_async.html";
iframe2a.src = "about:blank";
iframe2b.src = "about:blank";
break;
case 3:
// Load in the iframe page through unauthenticated proxy
// and discard the proxy authentication. We expect to see
// unauthenticated page content and just a single dialog.
ok(true, "doTest testNum 3");
expectedLoads = 1;
expectedDialogs = 1;
iframe1.src = exampleCom + "authenticate.sjs?"+
"user=user4name&"+
"pass=user4pass&"+
"realm=mochirealm4&"+
"proxy_user=proxy_user3&"+
"proxy_pass=proxy_pass3&"+
"proxy_realm=proxy_realm3";
break;
case 4:
// Reload the frame from previous step and pass the proxy authentication
// but cancel the WWW authentication. We should get the proxy=ok and WWW=fail
// content as a result.
ok(true, "doTest testNum 4");
expectedLoads = 1;
expectedDialogs = 2;
iframe1.contentDocument.location.reload();
break;
case 5:
// Same as the previous two steps but let the server generate
// huge content load to check http channel is capable to handle
// case when auth dialog is canceled or accepted before unauthenticated
// content data is load from the server. (This would be better to
// implement using delay of server response).
ok(true, "doTest testNum 5");
expectedLoads = 1;
expectedDialogs = 1;
iframe1.src = exampleCom + "authenticate.sjs?"+
"user=user5name&"+
"pass=user5pass&"+
"realm=mochirealm5&"+
"proxy_user=proxy_user4&"+
"proxy_pass=proxy_pass4&"+
"proxy_realm=proxy_realm4&"+
"huge=1";
break;
case 6:
// Reload the frame from the previous step and let the proxy
// authentication pass but WWW fail. We expect two dialogs
// and an unathentiocated page content load.
ok(true, "doTest testNum 6");
expectedLoads = 1;
expectedDialogs = 2;
iframe1.contentDocument.location.reload();
break;
case 7:
// Reload again and let pass all authentication dialogs.
// Check we get the authenticated content not broken by
// the unauthenticated content.
ok(true, "doTest testNum 7");
expectedLoads = 1;
expectedDialogs = 1;
iframe1.contentDocument.location.reload();
break;
case 8:
// Check we proccess all challenges sent by server when
// user cancels prompts
ok(true, "doTest testNum 8");
expectedLoads = 1;
expectedDialogs = 5;
iframe1.src = exampleCom + "authenticate.sjs?"+
"user=user6name&"+
"pass=user6pass&"+
"realm=mochirealm6&"+
"proxy_user=proxy_user5&"+
"proxy_pass=proxy_pass5&"+
"proxy_realm=proxy_realm5&"+
"huge=1&"+
"multiple=3";
break;
case 9:
finishTest();
return;
}
startCallbackTimer();
}
function handleDialog(doc, testNum)
{
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
var dialog = doc.getElementById("commonDialog");
switch (testNum)
{
case 1:
case 2:
dialog.acceptDialog();
break;
case 3:
dialog.cancelDialog();
break;
case 4:
if (expectedDialogs == 2)
dialog.acceptDialog();
else
dialog.cancelDialog();
break;
case 5:
dialog.cancelDialog();
break;
case 6:
if (expectedDialogs == 2)
dialog.acceptDialog();
else
dialog.cancelDialog();
break;
case 7:
dialog.acceptDialog();
break;
case 8:
if (expectedDialogs == 3 || expectedDialogs == 1)
dialog.acceptDialog();
else
dialog.cancelDialog();
break;
default:
ok(false, "Unhandled testNum "+testNum+" in handleDialog");
}
if (--expectedDialogs > 0)
startCallbackTimer();
}
function doCheck(testNum)
{
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
switch (testNum)
{
case 1:
ok(true, "doCheck testNum 1");
is(monitor.windowsRegistered, 3, "Registered 3 open dialogs");
var authok1 = iframe1.contentDocument.getElementById("ok").textContent;
var proxyok1 = iframe1.contentDocument.getElementById("proxy").textContent;
var authok2a = iframe2a.contentDocument.getElementById("ok").textContent;
var proxyok2a = iframe2a.contentDocument.getElementById("proxy").textContent;
var authok2b = iframe2b.contentDocument.getElementById("ok").textContent;
var proxyok2b = iframe2b.contentDocument.getElementById("proxy").textContent;
is(authok1, "PASS", "WWW Authorization OK, frame1");
is(authok2a, "PASS", "WWW Authorization OK, frame2a");
is(authok2b, "PASS", "WWW Authorization OK, frame2b");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
is(proxyok2a, "PASS", "Proxy Authorization OK, frame2a");
is(proxyok2b, "PASS", "Proxy Authorization OK, frame2b");
break;
case 2:
is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
ok(true, "doCheck testNum 2");
function checkIframe(frame) {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
var authok = frame.contentDocument.getElementById("ok").textContent;
var proxyok = frame.contentDocument.getElementById("proxy").textContent;
is(authok, "PASS", "WWW Authorization OK, " + frame.id);
is(proxyok, "PASS", "Proxy Authorization OK, " + frame.id);
}
checkIframe(iframe1.contentDocument.getElementById("iframe1"));
checkIframe(iframe1.contentDocument.getElementById("iframe2"));
checkIframe(iframe1.contentDocument.getElementById("iframe3"));
break;
case 3:
ok(true, "doCheck testNum 3");
is(monitor.windowsRegistered, 1, "Registered 1 open dialog");
var authok1 = iframe1.contentDocument.getElementById("ok").textContent;
var proxyok1 = iframe1.contentDocument.getElementById("proxy").textContent;
is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
is(proxyok1, "FAIL", "Proxy Authorization FAILED, frame1");
break;
case 4:
ok(true, "doCheck testNum 4");
is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
var authok1 = iframe1.contentDocument.getElementById("ok").textContent;
var proxyok1 = iframe1.contentDocument.getElementById("proxy").textContent;
is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
break;
case 5:
ok(true, "doCheck testNum 5");
is(monitor.windowsRegistered, 1, "Registered 1 open dialog");
var authok1 = iframe1.contentDocument.getElementById("ok").textContent;
var proxyok1 = iframe1.contentDocument.getElementById("proxy").textContent;
var footnote = iframe1.contentDocument.getElementById("footnote").textContent;
is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
is(proxyok1, "FAIL", "Proxy Authorization FAILED, frame1");
is(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
break;
case 6:
ok(true, "doCheck testNum 6");
is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
var authok1 = iframe1.contentDocument.getElementById("ok").textContent;
var proxyok1 = iframe1.contentDocument.getElementById("proxy").textContent;
var footnote = iframe1.contentDocument.getElementById("footnote").textContent;
is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
is(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
break;
case 7:
ok(true, "doCheck testNum 7");
is(monitor.windowsRegistered, 1, "Registered 1 open dialogs");
var authok1 = iframe1.contentDocument.getElementById("ok").textContent;
var proxyok1 = iframe1.contentDocument.getElementById("proxy").textContent;
var footnote = iframe1.contentDocument.getElementById("footnote").textContent;
is(authok1, "PASS", "WWW Authorization OK, frame1");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
is(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
break;
case 8:
ok(true, "doCheck testNum 8");
is(monitor.windowsRegistered, 5, "Registered 5 open dialogs");
var authok1 = iframe1.contentDocument.getElementById("ok").textContent;
var proxyok1 = iframe1.contentDocument.getElementById("proxy").textContent;
var footnote = iframe1.contentDocument.getElementById("footnote").textContent;
is(authok1, "PASS", "WWW Authorization OK, frame1");
is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
is(footnote, "This is a footnote after the huge content fill",
"Footnote present and loaded completely");
break;
default:
ok(false, "Unhandled testNum "+testNum+" in doCheck");
}
}
</script>
</head>
<body>
<iframe id="iframe1"></iframe>
<iframe id="iframe2a"></iframe>
<iframe id="iframe2b"></iframe>
</body>
</html>