/* * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.protocol.sip; import java.io.*; import java.text.*; import java.util.*; 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.Message; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; /** * A straightforward implementation of the basic instant messaging operation * set. * * @author Benoit Pradelle */ public class OperationSetBasicInstantMessagingSipImpl extends AbstractOperationSetBasicInstantMessaging { private static final Logger logger = Logger.getLogger(OperationSetBasicInstantMessagingSipImpl.class); /** * A list of processors registered for incoming sip messages. */ private Vector<SipMessageProcessor> messageProcessors = new Vector<SipMessageProcessor>(); /** * The provider that created us. */ private ProtocolProviderServiceSipImpl sipProvider = null; /** * A reference to the persistent presence operation set that we use * to match incoming messages to <tt>Contact</tt>s and vice versa. */ private OperationSetPresenceSipImpl opSetPersPresence = null; /** * Hashtable containing the CSeq of each discussion */ private long seqN = hashCode(); /** * Hashtable containing the message sent */ private Hashtable sentMsg = null; /** * It can be implemented in some servers. */ private final boolean offlineMessageSupported; /** * Gives access to presence states for the Sip protocol. */ private SipStatusEnum sipStatusEnum; /** * Creates an instance of this operation set. * @param provider a ref to the <tt>ProtocolProviderServiceImpl</tt> * that created us and that we'll use for retrieving the underlying aim * connection. */ OperationSetBasicInstantMessagingSipImpl( ProtocolProviderServiceSipImpl provider) { this.sipProvider = provider; this.sentMsg = new Hashtable(3); provider.addRegistrationStateChangeListener(new RegistrationStateListener()); offlineMessageSupported = provider.getAccountID().getAccountPropertyBoolean( "OFFLINE_MSG_SUPPORTED", false); sipProvider.registerMethodProcessor(Request.MESSAGE, new BasicInstantMessagingMethodProcessor()); this.sipStatusEnum = sipProvider.getSipStatusEnum(); } /** * Registers a SipMessageListener with this operation set so that it gets * notifications of successful message delivery, failure or reception of * incoming messages.. * * @param listener the <tt>SipMessageListener</tt> to register. */ void addMessageProcessor(SipMessageProcessor processor) { synchronized (this.messageProcessors) { if (!this.messageProcessors.contains(processor)) { this.messageProcessors.add(processor); } } } /** * Unregisters <tt>listener</tt> so that it won't receive any further * notifications upon successful message delivery, failure or reception of * incoming messages.. * * @param listener the <tt>SipMessageListener</tt> to unregister. */ void removeMessageProcessor(SipMessageProcessor processor) { synchronized (this.messageProcessors) { this.messageProcessors.remove(processor); } } public Message createMessage(String content, String contentType, String encoding, String subject) { return new MessageSipImpl(content, contentType, encoding, subject); } /** * Determines whether the protocol provider (or the protocol itself) support * sending and receiving offline messages. Most often this method would * return true for protocols that support offline messages and false for * those that don't. It is however possible for a protocol to support these * messages and yet have a particular account that does not (i.e. feature * not enabled on the protocol server). In cases like this it is possible * for this method to return true even when offline messaging is not * supported, and then have the sendMessage method throw an * OperationFailedException with code - OFFLINE_MESSAGES_NOT_SUPPORTED. * * @return <tt>true</tt> if the protocol supports offline messages and * <tt>false</tt> otherwise. */ public boolean isOfflineMessagingSupported() { return offlineMessageSupported; } /** * Determines whether the protocol supports the supplied content type * * @param contentType the type we want to check * @return <tt>true</tt> if the protocol supports it and * <tt>false</tt> otherwise. */ public boolean isContentTypeSupported(String contentType) { if(contentType.equals(DEFAULT_MIME_TYPE) || contentType.equals(HTML_MIME_TYPE)) return true; else return false; } /** * Sends the <tt>message</tt> to the destination indicated by the * <tt>to</tt> contact. * * @param to the <tt>Contact</tt> to send <tt>message</tt> to * @param message the <tt>Message</tt> to send. * @throws java.lang.IllegalStateException if the underlying stack is * not registered and initialized. * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an * instance of ContactImpl. */ public void sendInstantMessage(Contact to, Message message) throws IllegalStateException, IllegalArgumentException { if (! (to instanceof ContactSipImpl)) throw new IllegalArgumentException( "The specified contact is not a Sip contact." + to); assertConnected(); // offline message if (to.getPresenceStatus().equals( sipStatusEnum.getStatus(SipStatusEnum.OFFLINE)) && !offlineMessageSupported) { logger.debug("trying to send a message to an offline contact"); MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( message, to, MessageDeliveryFailedEvent.OFFLINE_MESSAGES_NOT_SUPPORTED, new Date()); fireMessageEvent(evt); return; } // create the message Request mes; try { mes = createMessage(to, message); } catch (OperationFailedException ex) { logger.error( "Failed to create the message." , ex); MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( message, to, MessageDeliveryFailedEvent.INTERNAL_ERROR, new Date()); fireMessageEvent(evt); return; } try { sendRequestMessage(mes, to, message); } catch(TransactionUnavailableException ex) { logger.error( "Failed to create messageTransaction.\n" + "This is most probably a network connection error." , ex); MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( message, to, MessageDeliveryFailedEvent.NETWORK_FAILURE, new Date()); fireMessageEvent(evt); return; } catch(SipException ex) { logger.error( "Failed to send the message." , ex); MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( message, to, MessageDeliveryFailedEvent.INTERNAL_ERROR, new Date()); fireMessageEvent(evt); return; } } void sendRequestMessage(Request mes, Contact to, Message message) throws TransactionUnavailableException, SipException { //check whether there's a cached authorization header for this //call id and if so - attach it to the request. // add authorization header CallIdHeader call = (CallIdHeader)mes.getHeader(CallIdHeader.NAME); String callid = call.getCallId(); AuthorizationHeader authorization = sipProvider .getSipSecurityManager() .getCachedAuthorizationHeader(callid); if(authorization != null) mes.addHeader(authorization); //Transaction ClientTransaction messageTransaction; SipProvider jainSipProvider = this.sipProvider.getDefaultJainSipProvider(); messageTransaction = jainSipProvider.getNewClientTransaction(mes); // send the message messageTransaction.sendRequest(); // we register the reference to this message to retrieve it when // we'll receive the response message String key = ((CallIdHeader)mes.getHeader(CallIdHeader.NAME)) .getCallId(); this.sentMsg.put(key, message); } /** * Construct a Request which represent a new message * * @param to the <tt>Contact</tt> to send <tt>message</tt> to * @param message the <tt>Message</tt> to send. * @return a Message Request destined to the contact * @throws OperationFailedException if an error occurred during * the creation of the request */ Request createMessage(Contact to, Message message) throws OperationFailedException { Address toAddress = null; try { toAddress = sipProvider.parseAddressString(to.getAddress()); } catch (ParseException exc) { //Shouldn't happen logger.error( "An unexpected error occurred while" + "constructing the address", exc); throw new OperationFailedException( "An unexpected error occurred while" + "constructing the address" , OperationFailedException.INTERNAL_ERROR , exc); } // Call ID CallIdHeader callIdHeader = this.sipProvider .getDefaultJainSipProvider().getNewCallId(); //CSeq CSeqHeader cSeqHeader = null; try { cSeqHeader = this.sipProvider.getHeaderFactory() .createCSeqHeader(seqN++, Request.MESSAGE); } catch (InvalidArgumentException ex) { //Shouldn't happen logger.error( "An unexpected error occurred while" + "constructing the CSeqHeadder", ex); throw new OperationFailedException( "An unexpected error occurred while" + "constructing the CSeqHeadder" , OperationFailedException.INTERNAL_ERROR , ex); } catch (ParseException exc) { //shouldn't happen logger.error( "An unexpected error occurred while" + "constructing the CSeqHeadder", exc); throw new OperationFailedException( "An unexpected error occurred while" + "constructing the CSeqHeadder" , OperationFailedException.INTERNAL_ERROR , exc); } //FromHeader and ToHeader String localTag = ProtocolProviderServiceSipImpl.generateLocalTag(); FromHeader fromHeader = null; ToHeader toHeader = null; try { //FromHeader fromHeader = this.sipProvider.getHeaderFactory() .createFromHeader( sipProvider.getOurSipAddress(toAddress), localTag); //ToHeader toHeader = this.sipProvider.getHeaderFactory() .createToHeader(toAddress, null); } catch (ParseException ex) { //these two should never happen. logger.error( "An unexpected error occurred while" + "constructing the FromHeader or ToHeader", ex); throw new OperationFailedException( "An unexpected error occurred while" + "constructing the FromHeader or ToHeader" , OperationFailedException.INTERNAL_ERROR , ex); } //ViaHeaders ArrayList<ViaHeader> viaHeaders = this.sipProvider.getLocalViaHeaders( toAddress); //MaxForwards MaxForwardsHeader maxForwards = this.sipProvider .getMaxForwardsHeader(); // Content params ContentTypeHeader contTypeHeader; ContentLengthHeader contLengthHeader; try { contTypeHeader = this.sipProvider.getHeaderFactory() .createContentTypeHeader(getType(message), getSubType(message)); if (! DEFAULT_MIME_ENCODING.equalsIgnoreCase(message.getEncoding())) contTypeHeader.setParameter("charset", message.getEncoding()); contLengthHeader = this.sipProvider.getHeaderFactory() .createContentLengthHeader(message.getSize()); } catch (ParseException ex) { //these two should never happen. logger.error( "An unexpected error occurred while" + "constructing the content headers", ex); throw new OperationFailedException( "An unexpected error occurred while" + "constructing the content headers" , OperationFailedException.INTERNAL_ERROR , ex); } catch (InvalidArgumentException exc) { //these two should never happen. logger.error( "An unexpected error occurred while" + "constructing the content length header", exc); throw new OperationFailedException( "An unexpected error occurred while" + "constructing the content length header" , OperationFailedException.INTERNAL_ERROR , exc); } Request req; try { req = this.sipProvider.getMessageFactory().createRequest( toHeader.getAddress().getURI(), Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards, contTypeHeader, message.getRawData()); } catch (ParseException ex) { //shouldn't happen logger.error( "Failed to create message Request!", ex); throw new OperationFailedException( "Failed to create message Request!" , OperationFailedException.INTERNAL_ERROR , ex); } req.addHeader(contLengthHeader); //User Agent UserAgentHeader userAgentHeader = sipProvider.getSipCommUserAgentHeader(); if(userAgentHeader != null) req.addHeader(userAgentHeader); return req; } /** * Parses the content type of a message and return the type * * @param msg the Message to scan * @return the type of the message */ private String getType(Message msg) { String type = msg.getContentType(); return type.substring(0, type.indexOf('/')); } /** * Parses the content type of a message and return the subtype * * @param msg the Message to scan * @return the subtype of the message */ private String getSubType(Message msg) { String subtype = msg.getContentType(); return subtype.substring(subtype.indexOf('/') + 1); } /** * Utility method throwing an exception if the stack is not properly * initialized. * @throws java.lang.IllegalStateException if the underlying stack is * not registered and initialized. */ private void assertConnected() throws IllegalStateException { if (this.sipProvider == null) throw new IllegalStateException( "The provider must be non-null and signed on the " + "service before being able to communicate."); if (!this.sipProvider.isRegistered()) throw new IllegalStateException( "The provider must be signed on the service before " + "being able to communicate."); } /** * Our listener that will tell us when we're registered to */ private class RegistrationStateListener implements RegistrationStateChangeListener { /** * The method is called by a ProtocolProvider implementation whenever * a change in the registration state of the corresponding provider had * occurred. * @param evt ProviderStatusChangeEvent the event describing the status * change. */ public void registrationStateChanged(RegistrationStateChangeEvent evt) { logger.debug("The provider changed state from: " + evt.getOldState() + " to: " + evt.getNewState()); if (evt.getNewState() == RegistrationState.REGISTERED) { opSetPersPresence = (OperationSetPresenceSipImpl) sipProvider .getOperationSet(OperationSetPersistentPresence.class); } } } /** * Class for listening incoming packets. */ private class BasicInstantMessagingMethodProcessor implements MethodProcessor { public boolean processDialogTerminated( DialogTerminatedEvent dialogTerminatedEvent) { // never fired return false; } public boolean processIOException(IOExceptionEvent exceptionEvent) { // never fired return false; } public boolean processTransactionTerminated( TransactionTerminatedEvent transactionTerminatedEvent) { // nothing to do return false; } /** * * @param timeoutEvent TimeoutEvent */ public boolean processTimeout(TimeoutEvent timeoutEvent) { synchronized (messageProcessors) { Iterator iter = messageProcessors.iterator(); while (iter.hasNext()) { SipMessageProcessor listener = (SipMessageProcessor)iter.next(); if(!listener.processTimeout(timeoutEvent, sentMsg)) return true; } } // this is normaly handled by the SIP stack logger.error("Timeout event thrown : " + timeoutEvent.toString()); if (timeoutEvent.isServerTransaction()) { logger.warn("The sender has probably not received our OK"); return false; } Request req = timeoutEvent.getClientTransaction().getRequest(); // get the content String content = null; try { content = new String(req.getRawContent(), getCharset(req)); } catch (UnsupportedEncodingException ex) { logger.warn("failed to convert the message charset", ex); content = new String(req.getRawContent()); } // to who this request has been sent ? ToHeader toHeader = (ToHeader) req.getHeader(ToHeader.NAME); if (toHeader == null) { logger.error("received a request without a to header"); return false; } Contact to = opSetPersPresence.resolveContactID( toHeader.getAddress().getURI().toString()); Message failedMessage = null; if (to == null) { logger.error( "timeout on a message sent to an unknown contact : " + toHeader.getAddress().getURI().toString()); //we don't know what message it concerns, so create a new //one failedMessage = createMessage(content); } else { // try to retrieve the original message String key = ((CallIdHeader)req.getHeader(CallIdHeader.NAME)) .getCallId(); failedMessage = (Message) sentMsg.get(key); if (failedMessage == null) { // should never happen logger.error("Couldn't find the sent message."); // we don't know what the message is so create a new one //based on the content of the failed request. failedMessage = createMessage(content); } } // error for delivering the message MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( // we don't know what message it concerns failedMessage, to, MessageDeliveryFailedEvent.INTERNAL_ERROR, new Date()); fireMessageEvent(evt); return true; } /** * Process a request from a distant contact * * @param requestEvent the <tt>RequestEvent</tt> containing the newly * received request. * @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 */ public boolean processRequest(RequestEvent requestEvent) { synchronized (messageProcessors) { Iterator iter = messageProcessors.iterator(); while (iter.hasNext()) { SipMessageProcessor listener = (SipMessageProcessor)iter.next(); if(!listener.processMessage(requestEvent)) return true; } } // get the content String content = null; Request req = requestEvent.getRequest(); try { content = new String(req.getRawContent(), getCharset(req)); } catch (UnsupportedEncodingException ex) { logger.debug("failed to convert the message charset"); content = new String(requestEvent.getRequest().getRawContent()); } // who sent this request ? FromHeader fromHeader = (FromHeader) requestEvent.getRequest().getHeader(FromHeader.NAME); if (fromHeader == null) { logger.error("received a request without a from header"); return false; } Contact from = opSetPersPresence.resolveContactID( fromHeader.getAddress().getURI().toString()); ContentTypeHeader ctheader = (ContentTypeHeader)req.getHeader(ContentTypeHeader.NAME); String ctype = null; String cencoding = null; if(ctheader == null) { ctype = DEFAULT_MIME_TYPE; } else { ctype = ctheader.getContentType() + "/" + ctheader.getContentSubType(); cencoding = ctheader.getParameter("charset"); } if(cencoding == null) cencoding = DEFAULT_MIME_ENCODING; Message newMessage = createMessage(content, ctype, cencoding, null); if (from == null) { logger.debug("received a message from an unknown contact: " + fromHeader.getAddress().getURI().toString()); //create the volatile contact from = opSetPersPresence .createVolatileContact(fromHeader.getAddress()); } // answer ok try { Response ok = sipProvider.getMessageFactory() .createResponse(Response.OK, requestEvent.getRequest()); SipProvider jainSipProvider = (SipProvider) requestEvent. getSource(); jainSipProvider.getNewServerTransaction( requestEvent.getRequest()).sendResponse(ok); } catch (ParseException exc) { logger.error("failed to build the response", exc); } catch (SipException exc) { logger.error("failed to send the response : " + exc.getMessage(), exc); } catch (InvalidArgumentException exc) { logger.debug("Invalid argument for createResponse : " + exc.getMessage(), exc); } // fire an event MessageReceivedEvent msgReceivedEvt = new MessageReceivedEvent( newMessage, from, new Date()); fireMessageEvent(msgReceivedEvt); return true; } /** * Process a response from a distant contact. * * @param responseEvent the <tt>ResponseEvent</tt> containing the newly * received SIP response. * @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 */ public boolean processResponse(ResponseEvent responseEvent) { synchronized (messageProcessors) { Iterator iter = messageProcessors.iterator(); while (iter.hasNext()) { SipMessageProcessor listener = (SipMessageProcessor)iter.next(); if(!listener.processResponse(responseEvent, sentMsg)) return true; } } Request req = responseEvent.getClientTransaction().getRequest(); int status = responseEvent.getResponse().getStatusCode(); // content of the response String content = null; try { content = new String(req.getRawContent(), getCharset(req)); } catch (UnsupportedEncodingException exc) { logger.debug("failed to convert the message charset", exc); content = new String(req.getRawContent()); } // to who did we send the original message ? ToHeader toHeader = (ToHeader) req.getHeader(ToHeader.NAME); if (toHeader == null) { // should never happen logger.error("send a request without a to header"); return false; } Contact to = opSetPersPresence.resolveContactID(toHeader.getAddress() .getURI().toString()); if (to == null) { logger.error( "Error received a response from an unknown contact : " + toHeader.getAddress().getURI().toString() + " : " + responseEvent.getResponse().getStatusCode() + " " + responseEvent.getResponse().getReasonPhrase()); // error for delivering the message MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( // we don't know what message it concerns createMessage(content), to, MessageDeliveryFailedEvent.INTERNAL_ERROR, new Date()); fireMessageEvent(evt); return false; } // we retrieve the original message String key = ((CallIdHeader)req.getHeader(CallIdHeader.NAME)) .getCallId(); Message newMessage = (Message) sentMsg.get(key); if (newMessage == null) { // should never happen logger.error("Couldn't find the message sent"); // error for delivering the message MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( // we don't know what message it is createMessage(content), to, MessageDeliveryFailedEvent.INTERNAL_ERROR, new Date()); fireMessageEvent(evt); return true; } // status 401/407 = proxy authentification if (status >= 400 && status != 401 && status != 407) { logger.info( responseEvent.getResponse().getStatusCode() + " " + responseEvent.getResponse().getReasonPhrase()); // error for delivering the message MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( newMessage, to, MessageDeliveryFailedEvent.NETWORK_FAILURE, new Date(), responseEvent.getResponse().getStatusCode() + " " + responseEvent.getResponse().getReasonPhrase()); fireMessageEvent(evt); sentMsg.remove(key); } else if (status == 401 || status == 407) { // proxy ask for authentification logger.debug( "proxy asks authentication : " + responseEvent.getResponse().getStatusCode() + " " + responseEvent.getResponse().getReasonPhrase()); ClientTransaction clientTransaction = responseEvent .getClientTransaction(); SipProvider sourceProvider = (SipProvider) responseEvent.getSource(); try { processAuthenticationChallenge(clientTransaction, responseEvent.getResponse(), sourceProvider); } catch (OperationFailedException ex) { logger.error("can't solve the challenge", ex); // error for delivering the message MessageDeliveryFailedEvent evt = new MessageDeliveryFailedEvent( newMessage, to, MessageDeliveryFailedEvent.NETWORK_FAILURE, new Date(), ex.getMessage()); fireMessageEvent(evt); sentMsg.remove(key); } } else if (status >= 200) { logger.debug( "Ack received from the network : " + responseEvent.getResponse().getStatusCode() + " " + responseEvent.getResponse().getReasonPhrase()); // we delivered the message MessageDeliveredEvent msgDeliveredEvt = new MessageDeliveredEvent( newMessage, to, new Date()); fireMessageEvent(msgDeliveredEvt); // we don't need this message anymore sentMsg.remove(key); } return true; } /** * Try to find a charset in a MESSAGE request for the * text content. If no charset is defined, the default charset * for text messages is returned. * * @param req the MESSAGE request in which to look for a charset * @return defined charset in the request or DEFAULT_MIME_ENCODING * if no charset is specified */ private String getCharset(Request req) { String charset = null; Header contentTypeHeader = req.getHeader(ContentTypeHeader.NAME); if (contentTypeHeader instanceof ContentTypeHeader) charset = ((ContentTypeHeader) contentTypeHeader) .getParameter("charset"); if (charset == null) charset = DEFAULT_MIME_ENCODING; return charset; } /** * Attempts to re-generate the corresponding request with the proper * credentials. * * @param clientTransaction the corresponding transaction * @param response the challenge * @param jainSipProvider the provider that received the challenge * * @throws OperationFailedException if processing the authentication * challenge fails. */ private void processAuthenticationChallenge( ClientTransaction clientTransaction, Response response, SipProvider jainSipProvider) throws OperationFailedException { try { logger.debug("Authenticating a message request."); ClientTransaction retryTran = sipProvider.getSipSecurityManager().handleChallenge( response , clientTransaction , jainSipProvider); if(retryTran == null) { logger.trace("No password supplied or error occured!"); return; } retryTran.sendRequest(); return; } catch (Exception exc) { logger.error("We failed to authenticate a message request.", exc); throw new OperationFailedException("Failed to authenticate" + "a message request" , OperationFailedException.INTERNAL_ERROR , exc); } } } }