1 /*
2  * sipim.cxx
3  *
4  * Support for SIP session mode IM
5  *
6  * Open Phone Abstraction Library (OPAL)
7  *
8  * Copyright (c) 2008 Post Increment
9  *
10  * The contents of this file are subject to the Mozilla Public License
11  * Version 1.0 (the "License"); you may not use this file except in
12  * compliance with the License. You may obtain a copy of the License at
13  * http://www.mozilla.org/MPL/
14  *
15  * Software distributed under the License is distributed on an "AS IS"
16  * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
17  * the License for the specific language governing rights and limitations
18  * under the License.
19  *
20  * The Original Code is Open Phone Abstraction Library.
21  *
22  * The Initial Developer of the Original Code is Post Increment
23  *
24  * Contributor(s): ______________________________________.
25  *
26  * $Revision: 26270 $
27  * $Author: rjongbloed $
28  * $Date: 2011-08-03 03:29:01 -0500 (Wed, 03 Aug 2011) $
29  */
30 
31 #include <ptlib.h>
32 #include <opal/buildopts.h>
33 
34 #ifdef __GNUC__
35 #pragma implementation "sipim.h"
36 #endif
37 
38 #include <ptlib/socket.h>
39 #include <ptclib/random.h>
40 #include <ptclib/pxml.h>
41 
42 #include <opal/transports.h>
43 #include <opal/mediatype.h>
44 #include <opal/mediafmt.h>
45 #include <opal/endpoint.h>
46 
47 #include <im/im.h>
48 #include <im/sipim.h>
49 #include <sip/sipep.h>
50 #include <sip/sipcon.h>
51 
52 #if OPAL_HAS_SIPIM
53 
54 const char SIP_IM[] = "sip-im";
55 
56 ////////////////////////////////////////////////////////////////////////////
57 
OpalSIPIMMediaType()58 OpalSIPIMMediaType::OpalSIPIMMediaType()
59   : OpalIMMediaType(SIP_IM, "message|sip")
60 {
61 }
62 
63 
64 /////////////////////////////////////////////////////////
65 //
66 //  SDP media description for the SIPIM type
67 //
68 //  A new class is needed for "message" due to the following differences
69 //
70 //  - the SDP type is "message"
71 //  - the transport is "sip"
72 //  - the format list is a SIP URL
73 //
74 
75 class SDPSIPIMMediaDescription : public SDPMediaDescription
76 {
77   PCLASSINFO(SDPSIPIMMediaDescription, SDPMediaDescription);
78   public:
79     SDPSIPIMMediaDescription(const OpalTransportAddress & address);
80     SDPSIPIMMediaDescription(const OpalTransportAddress & address, const OpalTransportAddress & _transportAddr, const PString & fromURL);
81 
GetSDPTransportType() const82     PCaselessString GetSDPTransportType() const
83     {
84       return "sip";
85     }
86 
CreateEmpty() const87     virtual SDPMediaDescription * CreateEmpty() const
88     {
89       return new SDPSIPIMMediaDescription(OpalTransportAddress());
90     }
91 
GetSDPMediaType() const92     virtual PString GetSDPMediaType() const
93     {
94       return "message";
95     }
96 
97     virtual PString GetSDPPortList() const;
98 
99     virtual void CreateSDPMediaFormats(const PStringArray &);
100     virtual bool PrintOn(ostream & str, const PString & connectString) const;
101     virtual void SetAttribute(const PString & attr, const PString & value);
102     virtual void ProcessMediaOptions(SDPMediaFormat & sdpFormat, const OpalMediaFormat & mediaFormat);
103     virtual void AddMediaFormat(const OpalMediaFormat & mediaFormat);
104 
105     virtual OpalMediaFormatList GetMediaFormats() const;
106 
107     // CreateSDPMediaFormat is used for processing format lists. MSRP always contains only "*"
CreateSDPMediaFormat(const PString &)108     virtual SDPMediaFormat * CreateSDPMediaFormat(const PString & ) { return NULL; }
109 
110     // FindFormat is used only for rtpmap and fmtp, neither of which are used for MSRP
FindFormat(PString &) const111     virtual SDPMediaFormat * FindFormat(PString &) const { return NULL; }
112 
113   protected:
114     OpalTransportAddress transportAddress;
115     PString fromURL;
116 };
117 
118 ////////////////////////////////////////////////////////////////////////////////////////////
119 
CreateSDPMediaDescription(const OpalTransportAddress & localAddress)120 SDPMediaDescription * OpalSIPIMMediaType::CreateSDPMediaDescription(const OpalTransportAddress & localAddress)
121 {
122   return new SDPSIPIMMediaDescription(localAddress);
123 }
124 
125 ///////////////////////////////////////////////////////////////////////////////////////////
126 
SDPSIPIMMediaDescription(const OpalTransportAddress & address)127 SDPSIPIMMediaDescription::SDPSIPIMMediaDescription(const OpalTransportAddress & address)
128   : SDPMediaDescription(address, SIP_IM)
129 {
130   SetDirection(SDPMediaDescription::SendRecv);
131 }
132 
SDPSIPIMMediaDescription(const OpalTransportAddress & address,const OpalTransportAddress & _transportAddr,const PString & _fromURL)133 SDPSIPIMMediaDescription::SDPSIPIMMediaDescription(const OpalTransportAddress & address, const OpalTransportAddress & _transportAddr, const PString & _fromURL)
134   : SDPMediaDescription(address, SIP_IM)
135   , transportAddress(_transportAddr)
136   , fromURL(_fromURL)
137 {
138   SetDirection(SDPMediaDescription::SendRecv);
139 }
140 
CreateSDPMediaFormats(const PStringArray &)141 void SDPSIPIMMediaDescription::CreateSDPMediaFormats(const PStringArray &)
142 {
143   formats.Append(new SDPMediaFormat(*this, OpalSIPIM));
144 }
145 
146 
PrintOn(ostream & str,const PString &) const147 bool SDPSIPIMMediaDescription::PrintOn(ostream & str, const PString & /*connectString*/) const
148 {
149   // call ancestor
150   if (!SDPMediaDescription::PrintOn(str, ""))
151     return false;
152 
153   return true;
154 }
155 
GetSDPPortList() const156 PString SDPSIPIMMediaDescription::GetSDPPortList() const
157 {
158   PIPSocket::Address addr; WORD port;
159   transportAddress.GetIpAndPort(addr, port);
160 
161   PStringStream str;
162   str << ' ' << fromURL << '@' << addr << ':' << port;
163 
164   return str;
165 }
166 
167 
SetAttribute(const PString &,const PString &)168 void SDPSIPIMMediaDescription::SetAttribute(const PString & /*attr*/, const PString & /*value*/)
169 {
170 }
171 
ProcessMediaOptions(SDPMediaFormat &,const OpalMediaFormat &)172 void SDPSIPIMMediaDescription::ProcessMediaOptions(SDPMediaFormat & /*sdpFormat*/, const OpalMediaFormat & /*mediaFormat*/)
173 {
174 }
175 
GetMediaFormats() const176 OpalMediaFormatList SDPSIPIMMediaDescription::GetMediaFormats() const
177 {
178   OpalMediaFormat sipim(OpalSIPIM);
179   sipim.SetOptionString("URL", fromURL);
180 
181   PTRACE(4, "SIPIM\tNew format is " << setw(-1) << sipim);
182 
183   OpalMediaFormatList fmts;
184   fmts += sipim;
185   return fmts;
186 }
187 
AddMediaFormat(const OpalMediaFormat & mediaFormat)188 void SDPSIPIMMediaDescription::AddMediaFormat(const OpalMediaFormat & mediaFormat)
189 {
190   if (!mediaFormat.IsTransportable() || !mediaFormat.IsValidForProtocol("sip") || mediaFormat.GetMediaType() != SIP_IM) {
191     PTRACE(4, "SIPIM\tSDP not including " << mediaFormat << " as it is not a valid SIPIM format");
192     return;
193   }
194 
195   SDPMediaFormat * sdpFormat = new SDPMediaFormat(*this, mediaFormat);
196   ProcessMediaOptions(*sdpFormat, mediaFormat);
197   AddSDPMediaFormat(sdpFormat);
198 }
199 
200 
201 ////////////////////////////////////////////////////////////////////////////////////////////
202 
CreateMediaSession(OpalConnection & conn,unsigned sessionID) const203 OpalMediaSession * OpalSIPIMMediaType::CreateMediaSession(OpalConnection & conn, unsigned sessionID) const
204 {
205   // as this is called in the constructor of an OpalConnection descendant,
206   // we can't use a virtual function on OpalConnection
207 
208   if (conn.GetPrefixName() *= "sip")
209     return new OpalSIPIMMediaSession(conn, sessionID);
210 
211   return NULL;
212 }
213 
214 ////////////////////////////////////////////////////////////////////////////////////////////
215 
OpalSIPIMMediaSession(OpalConnection & _conn,unsigned _sessionId)216 OpalSIPIMMediaSession::OpalSIPIMMediaSession(OpalConnection & _conn, unsigned _sessionId)
217 : OpalMediaSession(_conn, SIP_IM, _sessionId)
218 {
219   transportAddress = connection.GetTransport().GetLocalAddress();
220   localURL         = connection.GetLocalPartyURL();
221   remoteURL        = connection.GetRemotePartyURL();
222   callId           = connection.GetToken();
223 }
224 
OpalSIPIMMediaSession(const OpalSIPIMMediaSession & obj)225 OpalSIPIMMediaSession::OpalSIPIMMediaSession(const OpalSIPIMMediaSession & obj)
226   : OpalMediaSession(obj)
227 {
228   transportAddress = obj.transportAddress;
229   localURL         = obj.localURL;
230   remoteURL        = obj.remoteURL;
231   callId           = obj.callId;
232 }
233 
GetLocalMediaAddress() const234 OpalTransportAddress OpalSIPIMMediaSession::GetLocalMediaAddress() const
235 {
236   return transportAddress;
237 }
238 
239 
CreateSDPMediaDescription(const OpalTransportAddress & sdpContactAddress)240 SDPMediaDescription * OpalSIPIMMediaSession::CreateSDPMediaDescription(const OpalTransportAddress & sdpContactAddress)
241 {
242   return new SDPSIPIMMediaDescription(sdpContactAddress, transportAddress, localURL);
243 }
244 
245 
CreateMediaStream(const OpalMediaFormat & mediaFormat,unsigned sessionID,PBoolean isSource)246 OpalMediaStream * OpalSIPIMMediaSession::CreateMediaStream(const OpalMediaFormat & mediaFormat,
247                                                                          unsigned sessionID,
248                                                                          PBoolean isSource)
249 {
250   PTRACE(2, "SIPIM\tCreated " << (isSource ? "source" : "sink") << " media stream in " << (connection.IsOriginating() ? "originator" : "receiver") << " with local " << localURL << " and remote " << remoteURL);
251   return new OpalIMMediaStream(connection, mediaFormat, sessionID, isSource);
252 }
253 
254 
SetRemoteMediaAddress(const OpalTransportAddress &,const OpalMediaFormatList &)255 void OpalSIPIMMediaSession::SetRemoteMediaAddress(const OpalTransportAddress &, const OpalMediaFormatList &)
256 {
257 }
258 
259 
260 ////////////////////////////////////////////////////////////////////////////////////////////
261 
262 static PFactory<OpalIMContext>::Worker<OpalSIPIMContext> static_OpalSIPContext("sip");
263 
OpalSIPIMContext()264 OpalSIPIMContext::OpalSIPIMContext()
265 {
266   m_attributes.Set("rx-composition-indication-state",   "idle");
267   m_attributes.Set("tx-composition-indication-state",   "idle");
268   m_attributes.Set("acceptable-content-types",          "text/plain\ntext/html\napplication/im-iscomposing+xml");
269   m_rxCompositionTimeout.SetNotifier(PCREATE_NOTIFIER(OnRxCompositionTimerExpire));
270   m_txCompositionTimeout.SetNotifier(PCREATE_NOTIFIER(OnTxCompositionTimerExpire));
271   m_txIdleTimeout.SetNotifier(PCREATE_NOTIFIER(OnTxIdleTimerExpire));
272 }
273 
PopulateParams(SIPMessage::Params & params,OpalIM & message)274 void OpalSIPIMContext::PopulateParams(SIPMessage::Params & params, OpalIM & message)
275 {
276   params.m_localAddress    = message.m_from.AsString();
277   params.m_addressOfRecord = params.m_localAddress;
278   params.m_remoteAddress   = message.m_to.AsString();
279   params.m_id              = message.m_conversationId;
280   params.m_messageId       = message.m_messageId;
281 
282   switch (message.m_type) {
283     case OpalIM::CompositionIndication_Idle:    // RFC 3994
284     case OpalIM::CompositionIndication_Active:  // RFC 3994
285       {
286         bool toActive = message.m_type == OpalIM::CompositionIndication_Active;
287         params.m_contentType = "application/im-iscomposing+xml";
288         params.m_body = "<?xml version='1.0' encoding='UTF-8'?>\n"
289                         "<isComposing xmlns='urn:ietf:params:xml:ns:im-iscomposing'\n"
290                         "  xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>\n";
291 
292         params.m_body += PString("    <state>") + (toActive ? "active" : "idle") + "</state>\n";
293 
294         // DO NOT ENABLE THIS BECAUSE XLite barfs on it
295         //params.m_body += PString("    <lastactive>") + PTime().AsString(PTime::RFC3339) + "</lastactive>\n";
296 
297         params.m_body +=         "    <refresh>60</refresh>\n"
298                          "</isComposing>";
299       }
300       break;
301 
302     //case OpalIM::Disposition:  // RFC 5438
303     //  break;
304 
305     case OpalIM::Text:
306     default:
307       params.m_contentType = message.m_mimeType;
308       params.m_body        = message.m_body;
309       break;
310   }
311 }
312 
313 
InternalSendOutsideCall(OpalIM * message)314 OpalIMContext::SentStatus OpalSIPIMContext::InternalSendOutsideCall(OpalIM * message)
315 {
316   ResetTimers(*message);
317 
318   SIPEndPoint * ep = dynamic_cast<SIPEndPoint *>(m_manager->FindEndPoint("sip"));
319   if (ep == NULL) {
320     PTRACE(2, "OpalSIPIMContext\tAttempt to send SIP IM without SIP endpoint");
321     return SentNoTransport;
322   }
323 
324   SIPMessage::Params params;
325   PopulateParams(params, *message);
326 
327   return ep->SendMESSAGE(params) ? SentPending : SentNoTransport;
328 }
329 
330 
InternalSendInsideCall(OpalIM * message)331 OpalIMContext::SentStatus OpalSIPIMContext::InternalSendInsideCall(OpalIM * message)
332 {
333   ResetTimers(*message);
334 
335   PSafePtr<SIPConnection> conn = PSafePtrCast<OpalConnection, SIPConnection>(m_connection);
336   if (conn == NULL) {
337     PTRACE(2, "OpalSIPIMContext\tAttempt to send SIP IM on non-SIP connection");
338     return SentFailedGeneric;
339   }
340 
341   SIPMessage::Params params;
342   PopulateParams(params, *message);
343 
344   PSafePtr<SIPTransaction> transaction = new SIPMessage(*conn, params);
345   return transaction->Start() ? SentOK : SentFailedGeneric;
346 }
347 
348 
ResetTimers(OpalIM & message)349 void OpalSIPIMContext::ResetTimers(OpalIM & message)
350 {
351   if (message.m_type == OpalIM::Text) {
352     m_txCompositionTimeout.Stop(true);
353     m_txIdleTimeout.Stop(true);
354     m_attributes.Set("tx-composition-indication-state", "idle");
355   }
356 }
357 
358 #if P_EXPAT
359 
360 static PXML::ValidationInfo const CompositionIndicationValidation[] = {
361   { PXML::SetDefaultNamespace,        "urn:ietf:params:xml:ns:im-iscomposing" },
362   { PXML::ElementName,                "isComposing", },
363 
364   { PXML::OptionalElement,                   "state"       },
365   { PXML::OptionalElement,                   "lastactive"  },
366   { PXML::OptionalElement,                   "contenttype" },
367   { PXML::OptionalElementWithBodyMatchingEx, "refresh",    { "[0-9]*" } },
368 
369   { PXML::EndOfValidationList }
370 };
371 
372 #endif
373 
OnIncomingIM(OpalIM & message)374 OpalIMContext::SentStatus OpalSIPIMContext::OnIncomingIM(OpalIM & message)
375 {
376   if (message.m_mimeType == "application/im-iscomposing+xml") {
377 #if P_EXPAT
378     PXML xml;
379     PString error;
380     if (!xml.LoadAndValidate(message.m_body, CompositionIndicationValidation, error, PXML::WithNS)) {
381       PTRACE(2, "OpalSIPIMContext\tXML error: " << error);
382       return SentInvalidContent;
383     }
384     PString state = "idle";
385     int timeout = 15;
386 
387     PXMLElement * element = xml.GetRootElement()->GetElement("state");
388     if ((element != NULL) && (element->GetData().Trim() == "active"))
389       state = "active";
390 
391     element = xml.GetRootElement()->GetElement("refresh");
392     if (element != NULL)
393       timeout = element->GetData().Trim().AsInteger();
394 
395     if (state == m_attributes.Get("rx-composition-indication-state")) {
396       PTRACE(2, "OpalSIPIMContext\tcomposition indication refreshed");
397       return SentOK;
398     }
399     m_attributes.Set("rx-composition-indication-state", state);
400 
401     if (state == "active")
402       m_rxCompositionTimeout = timeout * 1000;
403     else
404       m_rxCompositionTimeout.Stop(true);
405 
406     OnCompositionIndicationChanged(state);
407 
408     return SentOK;
409 #else
410     PTRACE(2, "OpalSIPIMContext\tunsupported content type");
411     return SentInvalidContent;
412 #endif
413   }
414 
415   // receipt of text always indicated idle
416   m_rxCompositionTimeout.Stop(true);
417   OnCompositionIndicationTimeout();
418 
419   // forward the text
420   return OpalIMContext::OnIncomingIM(message);
421 }
422 
OnRxCompositionTimerExpire(PTimer &,INT)423 void OpalSIPIMContext::OnRxCompositionTimerExpire(PTimer &, INT)
424 {
425   m_manager->GetIMManager().OnCompositionIndicationTimeout(GetID());
426 }
427 
OnCompositionIndicationTimeout()428 void OpalSIPIMContext::OnCompositionIndicationTimeout()
429 {
430   if (m_attributes.Get("rx-composition-indication-state") != "idle") {
431     m_attributes.Set("rx-composition-indication-state", "idle");
432     OnCompositionIndicationChanged("idle");
433   }
434 }
435 
436 
437 /*
438 OpalIMContext::SentStatus OpalSIPIMContext::SendText(bool active)
439 {
440   m_txCompositionTimeout.Stop(true);
441   m_txIdleTimeout.Stop(true);
442   m_attributes.Set("tx-composition-indication-state", "idle");
443 }
444 */
445 
SendCompositionIndication(bool active)446 OpalIMContext::SentStatus OpalSIPIMContext::SendCompositionIndication(bool active)
447 {
448   bool currentState = !(m_attributes.Get("tx-composition-indication-state", "idle") == "idle");
449 
450   if (active == currentState)
451     return OpalIMContext::SentOK;
452 
453   if (active) {
454     m_attributes.Set("tx-composition-indication-state", "active");
455     m_txCompositionTimeout = 1000 * 60;
456     m_txIdleTimeout = 1000 * 15;
457 
458   }
459   else {
460     m_txCompositionTimeout.Stop(true);
461     m_txIdleTimeout.Stop(true);
462   }
463 
464   return OpalIMContext::SendCompositionIndication(active);
465 }
466 
467 
OnTxCompositionTimerExpire(PTimer &,INT)468 void OpalSIPIMContext::OnTxCompositionTimerExpire(PTimer &, INT)
469 {
470   OpalIMContext::SendCompositionIndication(true);
471   m_txCompositionTimeout.SetMilliSeconds(1000 * 60);
472 }
473 
OnTxIdleTimerExpire(PTimer &,INT)474 void OpalSIPIMContext::OnTxIdleTimerExpire(PTimer &, INT)
475 {
476   m_txCompositionTimeout.Stop(true);
477   OpalIMContext::SendCompositionIndication(false);
478 }
479 
480 ////////////////////////////////////////////////////////////////////////////////////////////
481 
482 #endif // OPAL_HAS_SIPIM
483 
484