From 353d53d75e05ec7aceafd5acfb39ff62da594461 Mon Sep 17 00:00:00 2001 From: Emil Ivov Date: Mon, 28 Sep 2009 10:05:58 +0000 Subject: [PATCH] Fixes handling of 403 responses during authentication in a way that guarantees that the user would be re-asked for a password. --- .../protocol/sip/ActiveCallsRepository.java | 6 + .../impl/protocol/sip/CallPeerSipImpl.java | 1 - .../impl/protocol/sip/CallSipImpl.java | 43 +++- .../OperationSetBasicTelephonySipImpl.java | 61 ++---- .../impl/protocol/sip/SipMessageFactory.java | 56 +++++- .../protocol/sip/SipRegistrarConnection.java | 74 +++---- .../sip/security/CredentialsCache.java | 31 ++- .../sip/security/CredentialsCacheEntry.java | 24 ++- .../sip/security/SipSecurityManager.java | 190 +++++++++++++----- 9 files changed, 323 insertions(+), 163 deletions(-) diff --git a/src/net/java/sip/communicator/impl/protocol/sip/ActiveCallsRepository.java b/src/net/java/sip/communicator/impl/protocol/sip/ActiveCallsRepository.java index a62096858..04974e7a4 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/ActiveCallsRepository.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/ActiveCallsRepository.java @@ -38,6 +38,12 @@ public class ActiveCallsRepository private Hashtable activeCalls = new Hashtable(); + /** + * Creates a new instance of this repository. + * + * @param opSet a reference to the + * OperationSetBasicTelephonySipImpl that craeted us. + */ public ActiveCallsRepository(OperationSetBasicTelephonySipImpl opSet) { this.parentOperationSet = opSet; diff --git a/src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java index fbc8a1873..ee75e464b 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java @@ -107,7 +107,6 @@ public CallPeerSipImpl(Address peerAddress, { this.peerAddress = peerAddress; this.call = owningCall; - call.addCallPeer(this); //create the uid this.peerID = String.valueOf( System.currentTimeMillis()) diff --git a/src/net/java/sip/communicator/impl/protocol/sip/CallSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/CallSipImpl.java index bb25920f8..d1557ca65 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/CallSipImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/CallSipImpl.java @@ -53,17 +53,31 @@ public class CallSipImpl */ private final SipMessageFactory messageFactory; + /** + * A reference to the OperationSetBasicTelephonySipImpl that + * created us; + */ + private final OperationSetBasicTelephonySipImpl parentOpSet; + /** * Crates a CallSipImpl instance belonging to sourceProvider and * initiated by CallCreator. * * @param sourceProvider the ProtocolProviderServiceSipImpl instance in the - * context of which this call has been created. + * context of which this call has been created. + * @param parentOpSet a reference to the operation set that's creating us + * and that we would be able to use for even dispatching. */ - protected CallSipImpl(ProtocolProviderServiceSipImpl sourceProvider) + protected CallSipImpl(ProtocolProviderServiceSipImpl sourceProvider, + OperationSetBasicTelephonySipImpl parentOpSet) { super(sourceProvider); this.messageFactory = sourceProvider.getMessageFactory(); + this.parentOpSet = parentOpSet; + + //let's add ourselves to the calls repo. we are doing it ourselves just + //to make sure that no one ever forgets. + parentOpSet.getActiveCallsRepository().addCall(this); } /** @@ -73,7 +87,7 @@ protected CallSipImpl(ProtocolProviderServiceSipImpl sourceProvider) * * @param callPeer the new CallPeer */ - public void addCallPeer(CallPeerSipImpl callPeer) + private void addCallPeer(CallPeerSipImpl callPeer) { if (callPeers.contains(callPeer)) return; @@ -431,8 +445,7 @@ private void attachSdpOffer(Request invite, CallPeerSipImpl callPeer) /** * Creates an SDP description that could be sent to peer and adds - * it to - * response. Provides a hook for this instance to take last + * it to response. Provides a hook for this instance to take last * configuration steps on a specific Response before it is sent to * a specific CallPeer as part of the execution of. * @@ -442,7 +455,7 @@ private void attachSdpOffer(Request invite, CallPeerSipImpl callPeer) * @throws OperationFailedException if we fail parsing call peer's media. * @throws ParseException if we try to attach invalid SDP to response. */ - private void attachSDPAnswer(Response response, CallPeerSipImpl peer) + private void attachSdpHoldAnswer(Response response, CallPeerSipImpl peer) throws OperationFailedException, ParseException { /* @@ -450,8 +463,7 @@ private void attachSDPAnswer(Response response, CallPeerSipImpl peer) * response to a call-hold invite is to be sent. */ - CallSession callSession = - ((CallSipImpl) peer.getCall()).getMediaCallSession(); + CallSession callSession = peer.getCall().getMediaCallSession(); String sdpAnswer = null; try @@ -487,8 +499,11 @@ private CallPeerSipImpl createCallPeerFor( { CallPeerSipImpl callPeer = new CallPeerSipImpl( containingTransaction.getDialog().getRemoteParty(), this); + addCallPeer(callPeer); - callPeer.setState( (containingTransaction instanceof ServerTransaction) + boolean incomingCall + = (containingTransaction instanceof ServerTransaction); + callPeer.setState( incomingCall ? CallPeerState.INCOMING_CALL : CallPeerState.INITIATING_CALL); @@ -496,6 +511,12 @@ private CallPeerSipImpl createCallPeerFor( callPeer.setFirstTransaction(containingTransaction); callPeer.setJainSipProvider(sourceProvider); + // notify everyone + parentOpSet.fireCallEvent( (incomingCall + ? CallEvent.CALL_RECEIVED + : CallEvent.CALL_INITIATED), + this); + return callPeer; } @@ -825,9 +846,9 @@ public synchronized void answerCallPeer(CallPeerSipImpl callPeer) throws OperationFailedException { Transaction transaction = callPeer.getFirstTransaction(); - Dialog dialog = callPeer.getDialog(); - if (transaction == null || !dialog.isServer()) + if (transaction == null || + !(transaction instanceof ServerTransaction)) { callPeer.setState(CallPeerState.DISCONNECTED); throw new OperationFailedException( diff --git a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicTelephonySipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicTelephonySipImpl.java index 401beae3b..3b7145f93 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicTelephonySipImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicTelephonySipImpl.java @@ -164,14 +164,10 @@ private synchronized CallSipImpl createOutgoingCall(Address calleeAddress, { assertRegistered(); - CallSipImpl call = new CallSipImpl(protocolProvider); + CallSipImpl call = new CallSipImpl(protocolProvider, this); - activeCallsRepository.addCall(call); call.invite(calleeAddress, cause); - // notify everyone - fireCallEvent( CallEvent.CALL_INITIATED, call); - return call; } @@ -185,6 +181,18 @@ public Iterator getActiveCalls() return activeCallsRepository.getActiveCalls(); } + /** + * Returns a reference to the {@link ActiveCallsRepository} that we are + * currently using. + * + * @return a reference to the {@link ActiveCallsRepository} that we are + * currently using. + */ + protected ActiveCallsRepository getActiveCallsRepository() + { + return activeCallsRepository; + } + /** * Resumes communication with a call peer previously put on hold. * @@ -1161,7 +1169,7 @@ private void processInvite(SipProvider sourceProvider, if (replacesHeader == null) { //this is a brand new call (not a transfered one) - callSipImpl = new CallSipImpl(protocolProvider); + callSipImpl = new CallSipImpl(protocolProvider, this); callSipImpl.processInvite(sourceProvider, serverTransaction); } else @@ -1879,47 +1887,6 @@ public synchronized void answerCallPeer(CallPeer peer) } - /** - * Creates a new call and call peer associated with - * containingTransaction - * - * @param containingTransaction the transaction that created the call. - * @param sourceProvider the provider that the containingTransaction belongs - * to. - * - * @return a new instance of a CallPeerSipImpl corresponding - * to the containingTransaction. - */ - private CallPeerSipImpl createCallPeerFor( - Transaction containingTransaction, SipProvider sourceProvider) - { - CallSipImpl call = new CallSipImpl(protocolProvider); - CallPeerSipImpl callPeer = - new CallPeerSipImpl( - containingTransaction.getDialog().getRemoteParty(), - call); - boolean incomingCall = - (containingTransaction instanceof ServerTransaction); - - callPeer.setState( - incomingCall ? - CallPeerState.INCOMING_CALL : - CallPeerState.INITIATING_CALL); - - callPeer.setDialog(containingTransaction.getDialog()); - callPeer.setFirstTransaction(containingTransaction); - callPeer.setJainSipProvider(sourceProvider); - - activeCallsRepository.addCall(call); - - // notify everyone - fireCallEvent( - incomingCall ? CallEvent.CALL_RECEIVED : CallEvent.CALL_INITIATED, - call); - - return callPeer; - } - /** * Returns a string representation of this OperationSetBasicTelephony * instance including information that would permit to distinguish it among diff --git a/src/net/java/sip/communicator/impl/protocol/sip/SipMessageFactory.java b/src/net/java/sip/communicator/impl/protocol/sip/SipMessageFactory.java index 0a7ef48f3..aa40e0a1c 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/SipMessageFactory.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/SipMessageFactory.java @@ -8,6 +8,7 @@ import gov.nist.javax.sip.header.*; import gov.nist.javax.sip.header.extensions.*; +import gov.nist.javax.sip.message.*; import java.text.*; import java.util.*; @@ -201,6 +202,7 @@ public Response createResponse(int statusCode, Request request, { Response response = this.wrappedFactory.createResponse(statusCode, request, contentType, content); + extractAndApplyDialogToTag((SIPRequest)request, response); return (Response)attachScSpecifics(response); } @@ -216,6 +218,7 @@ public Response createResponse(int statusCode, Request request, { Response response = this.wrappedFactory.createResponse(statusCode, request, contentType, content); + extractAndApplyDialogToTag((SIPRequest)request, response); return (Response)attachScSpecifics(response); } @@ -230,6 +233,7 @@ public Response createResponse(int statusCode, Request request) { Response response = this.wrappedFactory.createResponse(statusCode, request); + extractAndApplyDialogToTag((SIPRequest)request, response); return (Response)attachScSpecifics(response); } @@ -246,6 +250,51 @@ public Response createResponse(String responseParam) return (Response)attachScSpecifics(response); } + /** + * It appears that when the JAIN-SIP message factory creates responses it + * does not add the dialog to tag (if it exists) to the response. We + * therefore try some heavy-weight JAIN-SIP hacking in order to do this + * ourselves. + * + * @param request the SIPRequest that we are constructing a + * response to. + * @param response the Response that we have just constructed + * and that we'd like to patch with a To tag. + */ + private void extractAndApplyDialogToTag( + SIPRequest request, Response response) + { + ServerTransaction tran = (ServerTransaction)request.getTransaction(); + + //be extra cautious + if(tran == null) + return; + + Dialog dialog = tran.getDialog(); + + if(dialog == null) + return; + + String localDialogTag = dialog.getLocalTag(); + + ToHeader to = (ToHeader) response.getHeader(ToHeader.NAME); + + if( to == null) + return; + + try + { + to.setTag(localDialogTag); + } + catch (ParseException e) + { + //we just extracted the tag from a Dialog. This is therefore + //very unlikely to happen and we are going to just log it. + logger.debug("Failed to attach a to tag", e); + } + + } + /** * Attaches to message headers and object params that we'd like to * be present in absolutely all messages we create (like for example the @@ -253,7 +302,7 @@ public Response createResponse(String responseParam) * tag). * * @param message the message that we'd like to tag - * @return + * @return returns a reference to message for convenience reasons. */ private Message attachScSpecifics(Message message) { @@ -269,7 +318,7 @@ private Message attachScSpecifics(Message message) Response response = (Response)message; //if there's a from tag and this is a non-failure response, - //then we are adding a to tag. + //that still doesn't have a To tag, then we are adding a to tag. if(fromTag != null && fromTag.trim().length() > 0 && response.getStatusCode() > 100 && response.getStatusCode() < 300) @@ -660,6 +709,8 @@ private void reflectCauseOnEffect(javax.sip.message.Message cause, * @return a Header which represents the Replaces header contained * in the URI of the specified address; null if * no such header is present + * + * @throws OperationFailedException if we can't parse the replaces header. */ private Header stripReplacesHeader( Address address ) throws OperationFailedException @@ -710,7 +761,6 @@ private Header stripReplacesHeader( Address address ) * effect if the Call-ID has not been seen by our security manager. * * @param request the request that we'd like to try pre-authenticating. - * @param service the provider where the request originated. */ public void preAuthenticateRequest( Request request ) { diff --git a/src/net/java/sip/communicator/impl/protocol/sip/SipRegistrarConnection.java b/src/net/java/sip/communicator/impl/protocol/sip/SipRegistrarConnection.java index 2963fcfa0..734dad5c1 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/SipRegistrarConnection.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/SipRegistrarConnection.java @@ -928,25 +928,18 @@ else if (response.getStatusCode() == Response.NOT_IMPLEMENTED) { else if (response.getStatusCode() == Response.TRYING) { //do nothing } - //401 UNAUTHORIZED + //401 UNAUTHORIZED, + //407 PROXY_AUTHENTICATION_REQUIRED, + //403 FORBIDDEN else if (response.getStatusCode() == Response.UNAUTHORIZED || response.getStatusCode() - == Response.PROXY_AUTHENTICATION_REQUIRED) - { - processAuthenticationChallenge(clientTransaction - , response - , sourceProvider); - processed = true; - } - //403 FORBIDDEN - else if (response.getStatusCode() == Response.FORBIDDEN) + == Response.PROXY_AUTHENTICATION_REQUIRED + || response.getStatusCode() == Response.FORBIDDEN) { - processForbidden(clientTransaction - , response - , sourceProvider); + processAuthenticationChallenge( + clientTransaction, response, sourceProvider); processed = true; } - //errors else if ( response.getStatusCode() >= 400 ) { logger.error("Received an error response."); @@ -982,11 +975,24 @@ private void processAuthenticationChallenge( { logger.debug("Authenticating a Register request."); - ClientTransaction retryTran - = sipProvider.getSipSecurityManager().handleChallenge( - response, - clientTransaction, - jainSipProvider); + ClientTransaction retryTran; + + if( response.getStatusCode() == Response.UNAUTHORIZED + || response.getStatusCode() + == Response.PROXY_AUTHENTICATION_REQUIRED) + { + //respond to the challenge + retryTran = sipProvider.getSipSecurityManager().handleChallenge( + response, clientTransaction, jainSipProvider); + } + else + { + //we got a BAD PASSWD reply. send a new credential-less request + //in order to trigger a new challenge and rerequest a password. + retryTran = sipProvider.getSipSecurityManager() + .handleForbiddenResponse( + response, clientTransaction, jainSipProvider); + } if(retryTran == null) { @@ -1035,36 +1041,6 @@ private void processAuthenticationChallenge( } } - /** - * Makes sure that the last password used is removed from the cache, and - * notifies the user of the authentication failure.. - * - * @param clientTransaction the corresponding transaction - * @param response the challenge - * @param jainSipProvider the provider that received the challenge - */ - private void processForbidden( - ClientTransaction clientTransaction, - Response response, - SipProvider jainSipProvider) - { - logger.debug("Authenticating a Register request."); - - sipProvider.getSipSecurityManager().handleForbiddenResponse( - response - , clientTransaction - , jainSipProvider); - - - //tell the others we couldn't register - this.setRegistrationState( - RegistrationState.AUTHENTICATION_FAILED - , RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED - , "Received a "+Response.FORBIDDEN+" FORBIDDEN response while " - +"authenticating. Server returned error:" - + response.getReasonPhrase()); - } - /** * Processes a Request received on a SipProvider upon which this SipListener * is registered. diff --git a/src/net/java/sip/communicator/impl/protocol/sip/security/CredentialsCache.java b/src/net/java/sip/communicator/impl/protocol/sip/security/CredentialsCache.java index a1b9e94d2..597645e05 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/security/CredentialsCache.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/security/CredentialsCache.java @@ -7,6 +7,7 @@ package net.java.sip.communicator.impl.protocol.sip.security; import java.util.*; +import java.util.Map.*; import javax.sip.header.*; @@ -60,6 +61,33 @@ CredentialsCacheEntry get(String realm) return this.authenticatedRealms.get(realm); } + /** + * Returns the list of realms that branchID has been used to + * authenticate against. + * + * @param branchID the transaction branchID that we are looking for. + * + * @return the list of realms that branchID has been used to + * authenticate against. + */ + List getRealms(String branchID) + { + List realms = new LinkedList(); + + Iterator> credentials = + authenticatedRealms.entrySet().iterator(); + + while ( credentials.hasNext()) + { + Entry entry = credentials.next(); + + if (entry.getValue().containsBranchID(branchID)) + realms.add(entry.getKey()); + } + + return realms; + } + /** * Returns the credentials corresponding to the specified realm * or null if none could be found and removes the entry from the cache. @@ -89,7 +117,7 @@ void clear() * belongs to. * @param authorization the authorization header that we'd like to cache. */ - void cacheAuthorizationHeader(String callid, + void cacheAuthorizationHeader(String callid, AuthorizationHeader authorization) { authenticatedCalls.put(callid, authorization); @@ -108,5 +136,4 @@ AuthorizationHeader getCachedAuthorizationHeader(String callid) { return this.authenticatedCalls.get(callid); } - } diff --git a/src/net/java/sip/communicator/impl/protocol/sip/security/CredentialsCacheEntry.java b/src/net/java/sip/communicator/impl/protocol/sip/security/CredentialsCacheEntry.java index 8d1b51b73..09c53f69d 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/security/CredentialsCacheEntry.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/security/CredentialsCacheEntry.java @@ -25,8 +25,8 @@ class CredentialsCacheEntry /** * The transactionHistory list contains transactions where the entry * has been and that had not yet been responded to (or at least the response - * has not reached this class). The transactionHistory's elements are Strings - * corresponding to branch ids. + * has not reached this class). The transactionHistory's elements are + * Strings corresponding to branch id-s. */ private Vector transactionHistory = new Vector(); @@ -35,7 +35,7 @@ class CredentialsCacheEntry * know that we've seen it and don't try to authenticate with the same * credentials if we get an unauthorized response for it. * - * @param requestBranchID the id to add to the list of uncofirmed + * @param requestBranchID the id to add to the list of unconfirmed * transactions. */ void pushBranchID(String requestBranchID) @@ -50,11 +50,25 @@ void pushBranchID(String requestBranchID) * there is no point in keeping it. We can't get an unauthorized response * more than once in the same transaction. * - * @param responseBranchID the branchi id of the response to process. - * @return true if this entry hase been used for the transaction. + * @param responseBranchID the branch id of the response to process. + * @return true if this entry has been used for the transaction. */ boolean popBranchID(String responseBranchID) { return transactionHistory.remove(responseBranchID); } + + /** + * Determines whether these credentials have been used for the specified + * transaction. + * + * @param branchID the branch id of the transaction that we are looking for. + * + * @return true if this entry has been used for the transaction + * and false otherwise. + */ + boolean containsBranchID(String branchID) + { + return transactionHistory.contains(branchID); + } } diff --git a/src/net/java/sip/communicator/impl/protocol/sip/security/SipSecurityManager.java b/src/net/java/sip/communicator/impl/protocol/sip/security/SipSecurityManager.java index 59bce68de..e653c9558 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/security/SipSecurityManager.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/security/SipSecurityManager.java @@ -6,6 +6,9 @@ */ package net.java.sip.communicator.impl.protocol.sip.security; +import gov.nist.javax.sip.header.*; +import gov.nist.javax.sip.message.*; + import java.text.*; import java.util.*; @@ -16,6 +19,7 @@ import net.java.sip.communicator.impl.protocol.sip.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; +import net.kano.joscar.snaccmd.ssi.*; /** * The class handles authentication challenges, caches user credentials and @@ -97,8 +101,6 @@ public void setHeaderFactory(HeaderFactory headerFactory) * new transaction * @throws InvalidArgumentException if we fail to create a new header * containing user credentials. - * @throws ParseException if we fail to create a new header containing user - * credentials. * @throws NullPointerException if an argument or a header is null. * @throws OperationFailedException if we fail to acquire a password from * our security authority. @@ -109,7 +111,6 @@ public ClientTransaction handleChallenge( SipProvider transactionCreator) throws SipException, InvalidArgumentException, - ParseException, OperationFailedException, NullPointerException { @@ -124,34 +125,24 @@ public ClientTransaction handleChallenge( ListIterator authHeaders = null; - if (challenge == null || reoriginatedRequest == null) - { - throw new NullPointerException( - "A null argument was passed to handle challenge."); - } - if (challenge.getStatusCode() == Response.UNAUTHORIZED) { authHeaders = challenge.getHeaders(WWWAuthenticateHeader.NAME); + + // Remove all authorization headers from the request (we'll re-add + // them from cache) reoriginatedRequest.removeHeader(AuthorizationHeader.NAME); } else if (challenge.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED) { authHeaders = challenge.getHeaders(ProxyAuthenticateHeader.NAME); - reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME); - } - if (authHeaders == null) - { - throw new NullPointerException( - "Could not find WWWAuthenticate or ProxyAuthenticate headers"); + // Remove all authorization headers from the request (we'll re-add + // them from cache) + reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME); } - //Remove all authorization headers from the request (we'll re-add them - //from cache) - - //rfc 3261 says that the cseq header should be augmented for the new //request. do it here so that the new dialog (created together with //the new client transaction) takes it into account. @@ -216,7 +207,7 @@ else if (challenge.getStatusCode() { //this is the transaction that created the cc entry. if we //need to authenticate the same transaction then the - //credentials we supplied the first time we wrong. + //credentials we supplied the first time were wrong. //remove password and ask user again. SipActivator.getProtocolProviderFactory().storePassword( accountID, null); @@ -317,25 +308,132 @@ public SecurityAuthority getSecurityAuthority() } /** - * Makes sure that the password that was used for this forbidden response, - * is removed from the local cache and is not stored for future use. + * Handles a 403 Forbidden response. Contrary to the + * handleChallenge method this one would not attach an \ + * authentication header to the request since there was no challenge in + * the response.As a result the use of this method would result in sending + * one more request and receiving one more failure response. Not quite + * efficient ... but what do you want ... life is tough. * * @param forbidden the 401/407 challenge response * @param endedTransaction the transaction established by the challenged * request * @param transactionCreator the JAIN SipProvider that we should use to * create the new transaction. + * + * @return the client transaction that can be used to try and reregister. + * + * @throws InvalidArgumentException if we fail to create a new header + * containing user credentials. + * @throws TransactionUnavailableException if we get an exception white + * creating the new transaction */ - public void handleForbiddenResponse( + public ClientTransaction handleForbiddenResponse( Response forbidden, ClientTransaction endedTransaction, SipProvider transactionCreator) + throws InvalidArgumentException, + TransactionUnavailableException + { - //a request that we previously sent was mal-authenticated. empty the - //credentials cache so that we don't use the same credentials once more. + //now empty the cache because the request we previously sent was + //mal-authenticated. cachedCredentials.clear(); + + //also remove the stored password: + SipActivator.getProtocolProviderFactory().storePassword( + accountID, null); + + //now recreate a transaction so that we could start all over again. + + Request challengedRequest = endedTransaction.getRequest(); + Request reoriginatedRequest = (Request) challengedRequest.clone(); + + //remove the branch id so that we could use the request in a new + //transaction + removeBranchID(reoriginatedRequest); + + //extract the realms that we tried to authenticate with the previous + //request and remove the authorization headers. + List realms = removeAuthHeaders(reoriginatedRequest); + + //increment cseq (as per 3261) + CSeqHeader cSeq = + (CSeqHeader) reoriginatedRequest.getHeader( (CSeqHeader.NAME)); + cSeq.setSeqNumber(cSeq.getSeqNumber() + 1l); + + ClientTransaction retryTran = + transactionCreator.getNewClientTransaction(reoriginatedRequest); + + //create a credentials entry with an empty password so that we can + //store the transaction and when we get the next challenge notify the + //user that their password was wrong. + Iterator realmsIter = realms.iterator(); + while(realmsIter.hasNext()) + { + CredentialsCacheEntry ccEntry = createCcEntryWithStoredPassword(""); + ccEntry.pushBranchID(retryTran.getBranchId()); + cachedCredentials.cacheEntry(realmsIter.next(), ccEntry); + } + + logger.debug("Returning authorization transaction."); + return retryTran; + } + + + /** + * Removes all authorization (and proxy authorization) headers from + * request and returns the list of realms that they were about. + * + * @param request the request that we'd like to clear of all authorization + * headers. + * @return the List of realms that this request was supposed to + * authenticate against. + */ + private List removeAuthHeaders(Request request) + { + List realms = new LinkedList(); + + Iterator headers = ((SIPRequest)request).getHeaders(); + + removeAuthHeaders(headers, realms); + + request.removeHeader(AuthorizationHeader.NAME); + request.removeHeader(ProxyAuthorizationHeader.NAME); + + return realms; } + /** + * Adds realms from all authorization headers in the headers list + * into the realms list (for use from removeAuthHeaders(Request) + * only). The method also handles header lists and is recursive. + * + * @param headers the list of headers that we need to analyze. + * @param realms the list that we should fill with the realmswe encounter + * in all kinds of authorization headers. + */ + @SuppressWarnings("unchecked") //no way around it + private void removeAuthHeaders(Iterator headers, + List realms) + { + while(headers.hasNext()) + { + SIPHeader header = headers.next(); + + if(header instanceof AuthorizationHeader) + { + realms.add(((AuthorizationHeader)header).getRealm()); + } + //expand header lists + else if (header instanceof SIPHeaderList) + { + Iterator hdrListIter + = ((SIPHeaderList)header).iterator(); + removeAuthHeaders(hdrListIter, realms); + } + } + } /** * Generates an authorisation header in response to wwwAuthHeader. @@ -458,26 +556,30 @@ public void cacheCredentials(String realm, UserCredentials credentials) * new one, equal to the one that was top most. * * @param request the Request whose branchID we'd like to remove. - * - * @throws ParseException in case the host port or transport in the original - * request were malformed - * @throws InvalidArgumentException if the port in the original via header - * was invalid. */ private void removeBranchID(Request request) - throws ParseException, InvalidArgumentException + { ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); request.removeHeader(ViaHeader.NAME); - ViaHeader newViaHeader = headerFactory.createViaHeader( - viaHeader.getHost() - , viaHeader.getPort() - , viaHeader.getTransport() - , null); - - request.setHeader(newViaHeader); + ViaHeader newViaHeader; + try + { + newViaHeader = headerFactory.createViaHeader( + viaHeader.getHost() + , viaHeader.getPort() + , viaHeader.getTransport() + , null); + request.setHeader(newViaHeader); + } + catch (Exception exc) + { + // we are using the host port and transport of an existing Via + // header so it would be quite weird to get this exception. + logger.debug("failed to reset a Via header"); + } } /** @@ -486,13 +588,14 @@ private void removeBranchID(Request request) * * @param realm the realm that we'd like to obtain a * CredentialsCacheEntry for. + * @param reasonCode one of the fields defined in this class that indicate + * the reason for asking the user to enter a password. * * @return a newly created CredentialsCacheEntry corresponding to * the specified realm. */ private CredentialsCacheEntry createCcEntryWithNewCredentials( - String realm, - int reasonCode) + String realm, int reasonCode) { CredentialsCacheEntry ccEntry = new CredentialsCacheEntry(); @@ -505,11 +608,8 @@ private CredentialsCacheEntry createCcEntryWithNewCredentials( else defaultCredentials.setUserName(accountID.getUserID()); - UserCredentials newCredentials = - getSecurityAuthority().obtainCredentials( - realm, - defaultCredentials, - reasonCode); + UserCredentials newCredentials = getSecurityAuthority() + .obtainCredentials( realm, defaultCredentials, reasonCode); // in case user has canceled the login window if(newCredentials == null)