/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.protocol.sip;
import gov.nist.javax.sip.message.*;
import java.io.*;
import java.net.*;
import java.security.cert.*;
import java.text.*;
import java.util.*;
import javax.net.ssl.*;
import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
/**
* Contains all functionality that has anything to do with registering and
* maintaining registrations with a SIP Registrar.
*
* @author Emil Ivov
*/
public class SipRegistrarConnection
extends MethodProcessorAdapter
{
/**
* Our class logger.
*/
private static final Logger logger =
Logger.getLogger(SipRegistrarConnection.class);
/**
* A reference to the sip provider that created us.
*/
private ProtocolProviderServiceSipImpl sipProvider = null;
/**
* The SipURI containing the address port and transport of our registrar
* server.
*/
private SipURI registrarURI = null;
/**
* The registrar address host we are connecting to.
*/
private String registrarName = null;
/**
* The name of the property under which the user may specify the number of
* seconds that registrations take to expire.
*/
private static final String REGISTRATION_EXPIRATION =
"net.java.sip.communicator.impl.protocol.sip.REGISTRATION_EXPIRATION";
/**
* The default amount of time (in seconds) that registration take to
* expire or otherwise put - the number of seconds we wait before re-
* registering.
*/
private static final int DEFAULT_REGISTRATION_EXPIRATION = 600;
/**
* The amount of time (in seconds) that registration take to expire or
* otherwise put - the number of seconds we wait before re-registering.
*/
private int registrationsExpiration = DEFAULT_REGISTRATION_EXPIRATION;
/**
* Keeps our current registration state.
*/
private RegistrationState currentRegistrationState
= RegistrationState.UNREGISTERED;
/**
* The timer we use for rescheduling registrations.
*/
private Timer reRegisterTimer = new Timer();
/**
* A copy of our last sent register request. (used when unregistering)
*/
private Request registerRequest = null;
/**
* The next long to use as a cseq header value.
*/
private long nextCSeqValue = 1;
/**
* The client transaction that we used for sending the last REGISTER
* request.
*/
ClientTransaction regTrans = null;
/**
* Option for specifying keep-alive method
*/
private static final String KEEP_ALIVE_METHOD = "KEEP_ALIVE_METHOD";
/**
* Option for keep-alive interval
*/
private static final String KEEP_ALIVE_INTERVAL = "KEEP_ALIVE_INTERVAL";
/**
* Default value for keep-alive method - register
*/
private static final int KEEP_ALIVE_INTERVAL_DEFAULT_VALUE = 25;
/**
* An account property to force messaging even if MESSAGE method is not
* listed in AllowHeader.
*/
private static final String FORCE_MESSAGING_PROP = "FORCE_MESSAGING";
/**
* Specifies whether or not we should be using a route header in register
* requests. This field is specified by the REGISTERS_USE_ROUTE account
* property.
*/
private boolean useRouteHeader = false;
/**
* The sip address that we're currently behind (the one that corresponds to
* our account id). ATTENTION!!! This field must remain <tt>null</tt>
* when this protocol provider is configured as "No Regsitrar" account and
* only be initialized if we actually have a registrar.
*/
private Address ourSipAddressOfRecord = null;
/**
* callId must be unique from first register to last one, till we
* unregister.
*/
private CallIdHeader callIdHeader = null;
/**
* The transport to use.
*/
private String registrationTransport = null;
/**
* Registrar port;
*/
private int registrarPort = -1;
/**
* We remember the last received address that REGISTER OK is coming from.
* For security reasons if we are registered this is the address
* that all messages must come from.
*/
private InetAddress lastRegisterAddressReceived = null;
/**
* We remember the last received port that REGISTER OK is coming from.
* For security reasons if we are registered this is the port
* that all messages must come from.
*/
private int lastRegisterPortReceived = -1;
/**
* Creates a new instance of this class.
*
* @param registrarName the FQDN of the registrar we will
* be registering with.
* @param registrarPort the port of the registrar we will
* be registering with.
* @param registrationTransport the transport to use when sending our
* REGISTER request to the server.
* @param sipProviderCallback a reference to the
* ProtocolProviderServiceSipImpl instance that created us.
*/
public SipRegistrarConnection(
String registrarName,
int registrarPort,
String registrationTransport,
ProtocolProviderServiceSipImpl sipProviderCallback)
{
this.registrarPort = registrarPort;
this.registrationTransport = registrationTransport;
this.registrarName = registrarName;
this.sipProvider = sipProviderCallback;
//init expiration timeout
this.registrationsExpiration =
SipActivator.getConfigurationService().getInt(
REGISTRATION_EXPIRATION,
DEFAULT_REGISTRATION_EXPIRATION);
//init our address of record to save time later
getAddressOfRecord();
//now let's register ourselves as processor for REGISTER related
//messages.
sipProviderCallback.registerMethodProcessor(Request.REGISTER, this);
}
/**
* Empty constructor that we only have in order to allow for classes like
* SipRegistrarlessConnection to extend this class.
*/
protected SipRegistrarConnection()
{
}
/**
* Changes transport of registrar connection and recreates
* registrar URI.
* @param newRegistrationTransport the new transport.
*/
void setTransport(String newRegistrationTransport)
{
if(newRegistrationTransport.equals(registrationTransport))
return;
this.registrationTransport = newRegistrationTransport;
if(!registrationTransport.equals(ListeningPoint.UDP))
registrarURI = null; // re-create it
}
/**
* Sends the REGISTER request to the server specified in the constructor.
*
* @throws OperationFailedException with the corresponding error code
* if registration or construction of the Register request fail.
*/
void register()
throws OperationFailedException
{
// skip REGISTERING event if we are already registered
// we are refreshing our registration
if (getRegistrationState() != RegistrationState.REGISTERED)
setRegistrationState(RegistrationState.REGISTERING,
RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
null);
Request request;
try
{
//We manage the Call ID Header ourselves.The rest will be handled
//by our SipMessageFactory
if(callIdHeader == null)
callIdHeader = this.getJainSipProvider().getNewCallId();
request = sipProvider.getMessageFactory().createRegisterRequest(
getAddressOfRecord(), registrationsExpiration, callIdHeader,
getNextCSeqValue());
}
catch (Exception exc)
{
if(exc.getCause() instanceof SocketException
|| exc.getCause() instanceof IOException
|| exc.getCause() instanceof SSLHandshakeException)
{
if(exc.getCause().getCause() instanceof CertificateException
|| exc.getCause().getMessage()
.startsWith("Received fatal alert"))
{
setRegistrationState(RegistrationState.UNREGISTERED
, RegistrationStateChangeEvent.REASON_USER_REQUEST
, exc.getMessage());
return;
}
if(sipProvider.registerUsingNextAddress())
return;
}
//catches InvalidArgumentException, ParseExeption
//this should never happen so let's just log and bail.
logger.error("Failed to create a Register request." , exc);
setRegistrationState(RegistrationState.CONNECTION_FAILED,
RegistrationStateChangeEvent.REASON_INTERNAL_ERROR,
exc.getMessage());
if(exc instanceof OperationFailedException)
throw (OperationFailedException)exc;
else
throw new OperationFailedException(
"Failed to generate a from header for our register request.",
OperationFailedException.INTERNAL_ERROR, exc);
}
//Transaction
try
{
regTrans = getJainSipProvider().getNewClientTransaction(request);
}
catch (TransactionUnavailableException ex)
{
logger.error("Could not create a register transaction!\n"
+ "Check that the Registrar address is correct!",
ex);
setRegistrationState(RegistrationState.CONNECTION_FAILED,
RegistrationStateChangeEvent.REASON_INTERNAL_ERROR,
ex.getMessage());
throw new OperationFailedException(
"Could not create a register transaction!\n"
+ "Check that the Registrar address is correct!",
OperationFailedException.NETWORK_FAILURE,
ex);
}
try
{
regTrans.sendRequest();
}
//we sometimes get a null pointer exception here so catch them all
catch (Exception ex)
{
if(ex.getCause() instanceof SocketException
|| ex.getCause() instanceof IOException)
{
if(sipProvider.registerUsingNextAddress())
return;
}
logger.error("Could not send out the register request!", ex);
setRegistrationState(RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
, ex.getMessage());
throw new OperationFailedException(
"Could not send out the register request!"
, OperationFailedException.NETWORK_FAILURE
, ex);
}
this.registerRequest = request;
}
/**
* An ok here means that our registration has been accepted or terminated
* (depending on the corresponding REGISTER request). We change state
* notify listeners and (in the case of a new registration) schedule
* reregistration.
*
* @param clientTransatcion the ClientTransaction that we created when
* sending the register request.
* @param response the OK Response that we've just received.
*/
@SuppressWarnings("unchecked") //legacy jain-sip code for handling multiple
//headers of the same type
public void processOK(ClientTransaction clientTransatcion,
Response response)
{
//first extract the expires value that we requested
int requestedExpiration = 0;
Request register = clientTransatcion.getRequest();
//first check for an expires param in the contact header
ContactHeader contactHeader = (ContactHeader) register
.getHeader(ContactHeader.NAME);
if (contactHeader != null)
requestedExpiration = contactHeader.getExpires();
else
requestedExpiration = 0;
//Try the expires header only if there was no expiration interval in
//the contact address. (Bug report and fix thereof Frederic Fournier)
if(requestedExpiration <= 0)
{
ExpiresHeader expiresHeader = register.getExpires();
if (expiresHeader != null)
requestedExpiration = expiresHeader.getExpires();
}
FromHeader fromHeader = (FromHeader) register
.getHeader(FromHeader.NAME);
if(fromHeader != null && fromHeader.getAddress() != null)
{
if(sipProvider.setOurDisplayName(
fromHeader.getAddress().getDisplayName()))
this.ourSipAddressOfRecord = null;
}
//now check if the registrar has touched our expiration timeout in its
//response. Again, check contact headers first.
int grantedExpiration;
ContactHeader responseContactHdr = (ContactHeader) response.getHeader(
ContactHeader.NAME);
if (responseContactHdr != null)
{
grantedExpiration = responseContactHdr.getExpires();
}
else
{
//no luck there, try the expires header.
ExpiresHeader expiresHeader = response.getExpires();
if (expiresHeader != null)
{
grantedExpiration = expiresHeader.getExpires();
}
//still no luck - let's be tolerant and reuse the expires timeout we
//stated in our last request
else
{
grantedExpiration = requestedExpiration;
}
}
//If this is a response to a REGISTER request ending our registration
//then expires would be 0.
//fix by Luca Bincoletto <Luca.Bincoletto@tilab.com>
//we also take into account the requested expiration since if it was 0
//we don't really care what the server replied (I have an asterisk here
//that gives me 3600 even if I request 0).
if (grantedExpiration <= 0 || requestedExpiration <= 0)
{
if(logger.isDebugEnabled())
logger.debug("Account "
+ sipProvider.getAccountID().getDisplayName()
+ " unregistered!");
Boolean userRequest = (Boolean)SipApplicationData.getApplicationData(
register, SipApplicationData.KEY_USER_REQUEST);
boolean userRequestBool = false;
if(userRequest != null && userRequest)
userRequestBool = true;
setRegistrationState(RegistrationState.UNREGISTERED
, RegistrationStateChangeEvent.REASON_USER_REQUEST
, "Registration terminated.",
userRequestBool);
}
else
{
int scheduleTime = grantedExpiration;
// registration schedule interval can be forced to keep alive
// with setting property KEEP_ALIVE_METHOD to register and
// setting the interval with property KEEP_ALIVE_INTERVAL
// to value in seconds, both properties are account props
// this does not change expiration header
// If KEEP_ALIVE_METHOD is null we default send registers on
// interval of 25 seconds
String keepAliveMethod =
sipProvider.getAccountID().getAccountPropertyString(
KEEP_ALIVE_METHOD);
if((keepAliveMethod != null &&
keepAliveMethod.equalsIgnoreCase("register")))
{
int registrationInterval =
sipProvider.getAccountID().getAccountPropertyInt(
KEEP_ALIVE_INTERVAL, KEEP_ALIVE_INTERVAL_DEFAULT_VALUE);
if (registrationInterval < grantedExpiration)
{
scheduleTime = registrationInterval;
}
}
// remember the address and port from which we received this
// message
lastRegisterAddressReceived =
((SIPMessage)response).getRemoteAddress();
lastRegisterPortReceived = ((SIPMessage)response).getRemotePort();
//schedule a reregistration.
scheduleReRegistration(scheduleTime);
//update the set of supported services
ListIterator<AllowHeader> headerIter
= response.getHeaders(AllowHeader.NAME);
if(headerIter != null && headerIter.hasNext())
updateSupportedOperationSets(headerIter);
if(logger.isDebugEnabled()
&& getRegistrationState().equals(RegistrationState.REGISTERING))
logger.debug("Account "
+ sipProvider.getAccountID().getDisplayName()
+ " registered!");
setRegistrationState(
RegistrationState.REGISTERED
, RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
, null);
}
}
/**
* Checks a particular request is it coming from the same proxy we are
* currently using. A check for security reasons, preventing injection
* of other messages in the PP.
* @param request the request to check.
* @return <tt>false</tt> if the request doesn't belong to our register
* or if we cannot decide we keep the old behaviour.
*/
public boolean isRequestFromSameConnection(Request request)
{
SIPMessage msg = (SIPMessage)request;
if(msg.getRemoteAddress() != null
&& lastRegisterAddressReceived != null)
{
if(!msg.getRemoteAddress().equals(lastRegisterAddressReceived)
|| msg.getRemotePort() != lastRegisterPortReceived)
{
return false;
}
}
return true;
}
/**
* Sends a unregistered request to the registrar thus ending our
* registration.
* @throws OperationFailedException with the corresponding code if sending
* or constructing the request fails.
*/
public void unregister() throws OperationFailedException
{
unregisterInternal(true, false);
}
/**
* Sends a unregistered request to the registrar thus ending our
* registration.
* @throws OperationFailedException with the corresponding code if sending
* or constructing the request fails.
*/
public void unregister(boolean userRequest)
throws OperationFailedException
{
unregisterInternal(true, userRequest);
}
/**
* Sends a unregistered request to the registrar thus ending our
* registration.
*
* @param sendUnregister indicates whether we should actually send an
* unREGISTER request or simply set our state to UNREGISTERED.
*
* @throws OperationFailedException with the corresponding code if sending
* or constructing the request fails.
*/
private void unregisterInternal(boolean sendUnregister, boolean userRequest)
throws OperationFailedException
{
if (getRegistrationState() == RegistrationState.UNREGISTERED)
{
if (logger.isTraceEnabled())
logger.trace("Trying to unregister when already unresgistered");
return;
}
cancelPendingRegistrations();
if (this.registerRequest == null)
{
logger.error("Couldn't find the initial register request");
setRegistrationState(RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
, "Could not find the initial regiest request.");
throw new OperationFailedException(
"Could not find the initial register request."
, OperationFailedException.INTERNAL_ERROR);
}
setRegistrationState(RegistrationState.UNREGISTERING,
RegistrationStateChangeEvent.REASON_USER_REQUEST, "");
if(!sendUnregister)
return;
//We are apparently registered so send an un-Register request.
Request unregisterRequest;
try
{
unregisterRequest = sipProvider.getMessageFactory()
.createUnRegisterRequest(registerRequest, getNextCSeqValue());
}
catch (InvalidArgumentException ex)
{
logger.error("Unable to create an unREGISTER request.", ex);
//Shouldn't happen
setRegistrationState(
RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
, "Unable to set Expires Header");
throw new OperationFailedException(
"Unable to set Expires Header"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
if(userRequest)
{
SipApplicationData.setApplicationData(unregisterRequest,
SipApplicationData.KEY_USER_REQUEST, userRequest);
}
ClientTransaction unregisterTransaction = null;
try
{
unregisterTransaction = getJainSipProvider()
.getNewClientTransaction(unregisterRequest);
}
catch (TransactionUnavailableException ex)
{
logger.error("Unable to create a unregister transaction", ex);
setRegistrationState(
RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
, "Unable to create a unregister transaction");
throw new OperationFailedException(
"Unable to create a unregister transaction"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
try
{
// remove current call-id header
// on next register we will create new one
callIdHeader = null;
unregisterTransaction.sendRequest();
//if we're currently registered or in a process of unregistering
//we'll wait for an ok response before changing the status.
//otherwise we set it immediately.
if(!(getRegistrationState().equals(RegistrationState.REGISTERED) ||
getRegistrationState().equals(RegistrationState.UNREGISTERING)))
{
if (logger.isInfoEnabled())
logger.info("Setting state to UNREGISTERED.");
setRegistrationState(
RegistrationState.UNREGISTERED
, RegistrationStateChangeEvent.REASON_USER_REQUEST, null);
//kill the registration tran in case it is still active
if (regTrans != null
&& regTrans.getState().getValue()
<= TransactionState.PROCEEDING.getValue())
{
if (logger.isTraceEnabled())
logger.trace("Will try to terminate reg tran ...");
regTrans.terminate();
if (logger.isTraceEnabled())
logger.trace("Transaction terminated!");
}
}
}
catch (SipException ex)
{
logger.error("Failed to send unregister request", ex);
setRegistrationState(
RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
, "Unable to create a unregister transaction");
throw new OperationFailedException(
"Failed to send unregister request"
, OperationFailedException.INTERNAL_ERROR
, ex);
}
}
/**
* Returns the state of this connection.
* @return a RegistrationState instance indicating the state of our
* registration with the corresponding registrar.
*/
public RegistrationState getRegistrationState()
{
return currentRegistrationState;
}
/**
* Sets our registration state to <tt>newState</tt> and dispatches an event
* through the protocol provider service impl.
* <p>
* @param newState a reference to the RegistrationState that we're currently
* detaining.
* @param reasonCode one of the REASON_XXX error codes specified in
* {@link RegistrationStateChangeEvent}.
* @param reason a reason String further explaining the reasonCode.
*/
public void setRegistrationState(RegistrationState newState,
int reasonCode,
String reason)
{
this.setRegistrationState(newState, reasonCode, reason, false);
}
/**
* Sets our registration state to <tt>newState</tt> and dispatches an event
* through the protocol provider service impl.
* <p>
* @param newState a reference to the RegistrationState that we're currently
* detaining.
* @param reasonCode one of the REASON_XXX error codes specified in
* {@link RegistrationStateChangeEvent}.
* @param reason a reason String further explaining the reasonCode.
*/
public void setRegistrationState(RegistrationState newState,
int reasonCode,
String reason,
boolean userRequest)
{
if( currentRegistrationState.equals(newState) )
return;
RegistrationState oldState = currentRegistrationState;
this.currentRegistrationState = newState;
sipProvider.fireRegistrationStateChanged(
oldState, newState, reasonCode, reason, userRequest);
}
/**
* The task is started once a registration has been created. It is
* scheduled to run after the expiration timeout has come to an end when
* it will resend the REGISTER request.
*/
private class ReRegisterTask
extends TimerTask
{
/**
* Creates a new instance of the ReRegister task prepared to reregister
* us after the specified interval.
*/
public ReRegisterTask()
{}
/**
* Simply calls the register method.
*/
@Override
public void run()
{
try
{
if (getRegistrationState() == RegistrationState.REGISTERED)
register();
}
catch (OperationFailedException ex)
{
logger.error("Failed to reRegister", ex);
setRegistrationState(
RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_INTERNAL_ERROR
, "Failed to re register with the SIP server.");
}
}
}
/**
* Cancels all pending reregistrations. The method is useful when shutting
* down.
*/
private void cancelPendingRegistrations()
{
reRegisterTimer.cancel();
reRegisterTimer = null;
reRegisterTimer = new Timer();
}
/**
* Schedules a reregistration for after almost <tt>expires</tt>
* seconds. The method leaves a margin for all intervals, scheduling
* the registration for slightly earlier by reducing with 10% the number
* of seconds specified in the expires param.
* <p>
* @param expires the number of seconds that we specified in the
* expires header when registering.
*/
private void scheduleReRegistration(int expires)
{
ReRegisterTask reRegisterTask = new ReRegisterTask();
//java.util.Timer thinks in miliseconds and expires header contains
//seconds
//bug report and fix by Willem Romijn (romijn at lucent.com)
//We keep a margin of 10% when sending re-registrations (1000
//becomes 900)
reRegisterTimer.schedule(reRegisterTask, expires * 900);
}
/**
* Returns the next long to use as a cseq header value.
* @return the next long to use as a cseq header value.
*/
private long getNextCSeqValue()
{
return nextCSeqValue++;
}
/**
* Handles a NOT_IMPLEMENTED response sent in reply of our register request.
*
* @param transatcion the transaction that our initial register request
* belongs to.
* @param response our initial register request.
*/
public void processNotImplemented(ClientTransaction transatcion,
Response response)
{
setRegistrationState(
RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
, registrarName
+ " does not appear to be a sip registrar. (Returned a "
+"NOT_IMPLEMENTED response to a register request)");
}
/**
* Analyzes the content of the <tt>allowHeader</tt> and determines whether
* some of the operation sets in our provider need to be disabled.
*
* @param headerIter the list of <tt>AllowHeader</tt>s that we need to
* analyze for supported features.
*/
private void updateSupportedOperationSets(
ListIterator<AllowHeader> headerIter)
{
HashSet<String> set = new HashSet<String>();
while(headerIter.hasNext())
{
set.add(headerIter.next().getMethod());
}
//Emil XXX: I am not sure how this would work out with most providers
//so we are going to only start by implementing this feature for IM.
if ( !set.contains(Request.MESSAGE)
&& !sipProvider.getAccountID()
.getAccountPropertyBoolean(FORCE_MESSAGING_PROP, false))
{
sipProvider.removeSupportedOperationSet(
OperationSetBasicInstantMessaging.class);
}
}
/**
* Returns the JAIN SIP provider that should be used for communication with
* our current registrar.
*
* @return the JAIN SIP provider that should be used for communication with
* our current registrar.
*/
public SipProvider getJainSipProvider()
{
return sipProvider.getJainSipProvider(getTransport());
}
/**
* Returns the transport that this connection is currently using to
* communicate with the Registrar.
*
* @return the transport that this connection is using.
*/
public String getTransport()
{
return this.registrationTransport;
}
/**
* Analyzes the incoming <tt>responseEvent</tt> and then forwards it to the
* proper event handler.
*
* @param responseEvent the responseEvent that we received
* ProtocolProviderService.
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
@Override
public boolean processResponse(ResponseEvent responseEvent)
{
ClientTransaction clientTransaction = responseEvent
.getClientTransaction();
Response response = responseEvent.getResponse();
SipProvider sourceProvider = (SipProvider)responseEvent.getSource();
boolean processed = false;
//OK
if (response.getStatusCode() == Response.OK) {
processOK(clientTransaction, response);
processed = true;
}
//NOT_IMPLEMENTED
else if (response.getStatusCode() == Response.NOT_IMPLEMENTED) {
processNotImplemented(clientTransaction, response);
processed = true;
}
//Trying
else if (response.getStatusCode() == Response.TRYING) {
//do nothing
}
//401 UNAUTHORIZED,
//407 PROXY_AUTHENTICATION_REQUIRED,
//403 FORBIDDEN
else if (response.getStatusCode() == Response.UNAUTHORIZED
|| response.getStatusCode()
== Response.PROXY_AUTHENTICATION_REQUIRED
|| response.getStatusCode() == Response.FORBIDDEN)
{
processAuthenticationChallenge(
clientTransaction, response, sourceProvider);
processed = true;
}
else if (response.getStatusCode() == Response.INTERVAL_TOO_BRIEF)
{
processIntervalTooBrief(response);
processed = true;
}
else if ( response.getStatusCode() >= 400 )
{
logger.error("Received an error response ("
+ response.getStatusCode() + ")" );
int registrationStateReason =
RegistrationStateChangeEvent.REASON_NOT_SPECIFIED;
if(response.getStatusCode() == Response.NOT_FOUND)
registrationStateReason =
RegistrationStateChangeEvent.REASON_NON_EXISTING_USER_ID;
//tell the others we couldn't register
this.setRegistrationState(
RegistrationState.CONNECTION_FAILED
, registrationStateReason
, "Received an error while trying to register. "
+ "Server returned error:" + response.getReasonPhrase()
);
processed = true;
}
//ignore everything else.
return processed;
}
/**
* Attempts to re-generate the corresponding request with the proper
* credentials and terminates the call if it fails.
*
* @param clientTransaction the corresponding transaction
* @param response the challenge
* @param jainSipProvider the provider that received the challenge
*/
private void processAuthenticationChallenge(
ClientTransaction clientTransaction,
Response response,
SipProvider jainSipProvider)
{
try
{
if (logger.isDebugEnabled())
logger.debug("Authenticating a Register request.");
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
{
//if we're in certificate authentication mode, a 403 probably
//means that the certificate didn't match. stop connecting.
String certId = sipProvider.getAccountID()
.getAccountPropertyString(
ProtocolProviderFactory.CLIENT_TLS_CERTIFICATE);
if(certId != null)
{
//tell the others we couldn't register
this.setRegistrationState(
RegistrationState.AUTHENTICATION_FAILED,
RegistrationStateChangeEvent
.REASON_NON_EXISTING_USER_ID,
"We failed to authenticate with the server."
);
return;
}
//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)
{
if (logger.isTraceEnabled())
logger.trace("No password supplied or error occured!");
unregister(false);
return;
}
//the security manager has most probably changed the sequence number
//so let's make sure we update it here.
updateRegisterSequenceNumber(retryTran);
retryTran.sendRequest();
return;
}
catch (OperationFailedException exc)
{
if(exc.getErrorCode()
== OperationFailedException.AUTHENTICATION_CANCELED)
{
this.setRegistrationState(
RegistrationState.UNREGISTERED,
RegistrationStateChangeEvent.REASON_USER_REQUEST,
"User has canceled the authentication process.");
}
else if (exc.getErrorCode() == OperationFailedException.FORBIDDEN)
{
this.setRegistrationState(
RegistrationState.CONNECTION_FAILED,
RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED,
exc.getMessage());
}
else
{
//tell the others we couldn't register
this.setRegistrationState(
RegistrationState.AUTHENTICATION_FAILED,
RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED,
"We failed to authenticate with the server."
);
}
}
catch (Exception exc)
{
logger.error("We failed to authenticate a Register request.", exc);
//tell the others we couldn't register
this.setRegistrationState(
RegistrationState.AUTHENTICATION_FAILED
, RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED
, "We failed to authenticate with the server."
);
}
}
/**
* Processes a Request received on a SipProvider upon which this SipListener
* is registered.
* <p>
*
* @param requestEvent requestEvent fired from the SipProvider to the
* SipListener representing a Request received from the network.
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
@Override
public boolean processRequest(RequestEvent requestEvent)
{
/** @todo send not implemented */
return false;
}
/**
* Processes a retransmit or expiration Timeout of an underlying
* {@link Transaction}handled by this SipListener.
*
* @param timeoutEvent the timeoutEvent received indicating either the
* message retransmit or transaction timed out.
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
@Override
public boolean processTimeout(TimeoutEvent timeoutEvent)
{
if(sipProvider.registerUsingNextAddress())
return false;
// don't alert the user if we're already off
if (!getRegistrationState().equals(RegistrationState.UNREGISTERED))
{
setRegistrationState(RegistrationState.CONNECTION_FAILED,
RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
"A timeout occurred while trying to connect to the server.");
}
return true;
}
/**
* Process an asynchronously reported IO Exception.
*
* @param exceptionEvent The Exception event that is reported to the
* application.
* @return <tt>true</tt> if the specified event has been handled by this
* processor and shouldn't be offered to other processors registered
* for the same method; <tt>false</tt>, otherwise
*/
@Override
public boolean processIOException(IOExceptionEvent exceptionEvent)
{
setRegistrationState(
RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
, "An error occurred while trying to connect to the server."
+ "[" + exceptionEvent.getHost() + "]:"
+ exceptionEvent.getPort() + "/"
+ exceptionEvent.getTransport());
return true;
}
/**
* Process error 423 Interval Too Brief. If there is minimum interval
* specified use it. Check the specified interval is greater than the one
* we used in our register.
* @param response the response containing the min expires header.
*/
private void processIntervalTooBrief(Response response)
{
// interval is too brief, if we have specified correct interval
// in the response use it and re-register
MinExpiresHeader header =
(MinExpiresHeader)response.getHeader(MinExpiresHeader.NAME);
if(header != null)
{
int expires = header.getExpires();
if(expires > registrationsExpiration)
{
registrationsExpiration = expires;
try
{
register();
return;
}
catch (Throwable e)
{
logger.error("Cannot send register!", e);
setRegistrationState(
RegistrationState.CONNECTION_FAILED,
RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
"A timeout occurred while trying to " +
"connect to the server.");
return;
}
}
}
//tell the others we couldn't register
this.setRegistrationState(
RegistrationState.CONNECTION_FAILED
, RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
, "Received an error while trying to register. "
+ "Server returned error:" + response.getReasonPhrase()
);
}
/**
* Returns a string representation of this connection instance
* instance including information that would permit to distinguish it among
* other sip listeners when reading a log file.
* <p>
* @return a string representation of this operation set.
*/
@Override
public String toString()
{
String className = getClass().getName();
try
{
className = className.substring(className.lastIndexOf('.') + 1);
}
catch (Exception ex)
{
// we don't want to fail in this method because we've messed up
//something with indexes, so just ignore.
}
return className + "-[dn=" + sipProvider.getOurDisplayName()
+" addr="+getAddressOfRecord() + "]";
}
/**
* Updates our local sequence counter based on the value in the CSeq header
* of the request that originated the <tt>lastClientTran</tt> transation.
* The method is used after running an authentication challenge through
* the security manager. The Security manager would manually increment the
* CSeq number of the request so we need to update our local counter or
* otherwise the next REGISTER we send would have a wrong CSeq.
*
* @param lastClientTran the transaction that we should be using to update
* our local sequence number
*/
private void updateRegisterSequenceNumber(ClientTransaction lastClientTran)
{
Request req = lastClientTran.getRequest();
CSeqHeader cSeqHeader = (CSeqHeader)req.getHeader(CSeqHeader.NAME);
long sequenceNumber = cSeqHeader.getSeqNumber();
//sequenceNumber is the value of the CSeq header in the request we just
//sent so the next CSeq Value should be set to seqNum + 1.
this.nextCSeqValue = sequenceNumber + 1;
}
/**
* Determines whether Register requests should be using a route header. The
* return value of this method is specified by the REGISTERS_USE_ROUTE
* account property.
*
* Jeroen van Bemmel: The reason this may needed, is that standards-
* compliant registrars check the domain in the request URI. If it contains
* an IP address, some registrars are unable to match/process it (they may
* forward instead, and get into a forwarding loop)
*
* @return true if we should be using a route header.
*/
public boolean isRouteHeaderEnabled()
{
return useRouteHeader;
}
/**
* Returns true if this is a fake connection that is not actually using
* a registrar. This method should be overridden in
* <tt>SipRegistrarlessConnection</tt> and return <tt>true</tt> in there.
*
* @return true if this connection is really using a registrar and
* false if it is a fake connection that doesn't really use a registrar.
*/
public boolean isRegistrarless()
{
return false;
}
/**
* The registrar URI we use for registrar connection.
* @return the registrar URI.
* * @throws ParseException in case the specified registrar address is not a
* valid registrar address.
*/
public SipURI getRegistrarURI()
throws ParseException
{
if(registrarURI == null)
{
registrarURI = sipProvider.getAddressFactory().createSipURI(
null, this.registrarName);
if(registrarPort != ListeningPoint.PORT_5060)
registrarURI.setPort(registrarPort);
if(!registrationTransport.equals(ListeningPoint.UDP)
&& !registrationTransport.equals(ListeningPoint.TLS))
{
registrarURI.setTransportParam(registrationTransport);
}
}
return registrarURI;
}
/**
* Returns the address of record that we are using to register against our
* registrar or null if this is a fake or "Registrarless" connection. If
* our are trying to obtain an address to put in your from header and don't
* no what to do in the case of registrarless accounts - think about using
* <tt>ProtocolProviderServiceSipImpl.createAddressOfRecord()</tt>.
*
* @return our Address Of Record
*/
public Address getAddressOfRecord()
{
//if we have a registrar we should return our Address of record here.
if(this.ourSipAddressOfRecord != null)
return this.ourSipAddressOfRecord;
// the connection would not have an address of record if it does not
// have a registrar.
if(isRegistrarless())
return null;
//create our own address.
String ourUserID =
sipProvider.getAccountID().getAccountPropertyString(
ProtocolProviderFactory.USER_ID);
String sipUriHost = null;
if( ourUserID.indexOf("@") != -1
&& ourUserID.indexOf("@") < ourUserID.length() -1 )
{
//use the domain in the SIP URI if possible.
sipUriHost = ourUserID.substring( ourUserID.indexOf("@") + 1 );
ourUserID = ourUserID.substring( 0, ourUserID.indexOf("@") );
}
//if there was no domain name in the SIP URI use the registrar address
if(sipUriHost == null)
sipUriHost = registrarName;
SipURI ourSipURI;
try
{
ourSipURI = sipProvider.getAddressFactory().createSipURI(
ourUserID, sipUriHost);
ourSipAddressOfRecord = sipProvider.getAddressFactory()
.createAddress(sipProvider.getOurDisplayName(), ourSipURI);
ourSipAddressOfRecord.setDisplayName(sipProvider.getOurDisplayName());
}
catch (ParseException ex)
{
throw new IllegalArgumentException(
"Could not create a SIP URI for user "
+ ourUserID + "@" + sipUriHost
+ " and registrar "
+ registrarName);
}
return ourSipAddressOfRecord;
}
}