/[resiprocate]/main/resip/dum/Dialog.cxx
ViewVC logotype

Annotation of /main/resip/dum/Dialog.cxx

Parent Directory Parent Directory | Revision Log Revision Log


Revision 8411 - (hide annotations) (download)
Tue Feb 10 13:21:21 2009 UTC (10 years, 9 months ago) by jmatthewsr
File MIME type: text/plain
File size: 40241 byte(s)
Exception in DUM caused by a subscription based dialog that receives a 200Ok to NOTIFY with a cseq method = INVITE.  (DUM) NOTIFY -> , (DUM) <- 200Ok cseq=INVITE.
1 derek 5283 #include "resip/stack/Contents.hxx"
2     #include "resip/stack/Helper.hxx"
3     #include "resip/stack/SipMessage.hxx"
4 jason 5276 #include "resip/dum/AppDialog.hxx"
5     #include "resip/dum/BaseCreator.hxx"
6     #include "resip/dum/ClientAuthManager.hxx"
7     #include "resip/dum/ClientInviteSession.hxx"
8     #include "resip/dum/ClientSubscription.hxx"
9     #include "resip/dum/Dialog.hxx"
10     #include "resip/dum/DialogUsageManager.hxx"
11     #include "resip/dum/MasterProfile.hxx"
12     #include "resip/dum/InviteSessionCreator.hxx"
13     #include "resip/dum/InviteSessionHandler.hxx"
14     #include "resip/dum/ServerInviteSession.hxx"
15     #include "resip/dum/ServerSubscription.hxx"
16     #include "resip/dum/SubscriptionHandler.hxx"
17     #include "resip/dum/UsageUseException.hxx"
18     #include "rutil/Logger.hxx"
19     #include "rutil/Inserter.hxx"
20     #include "rutil/WinLeakCheck.hxx"
21 davidb 2603
22     #define RESIPROCATE_SUBSYSTEM Subsystem::DUM
23    
24 ken 2520 using namespace resip;
25     using namespace std;
26    
27 jason 4010 Dialog::Dialog(DialogUsageManager& dum, const SipMessage& msg, DialogSet& ds)
28 derek 2995 : mDum(dum),
29 derek 2867 mDialogSet(ds),
30 derek 3089 mId("INVALID", "INVALID", "INVALID"),
31 jason 2588 mClientSubscriptions(),
32 derek 3041 mServerSubscriptions(),
33 jason 2588 mInviteSession(0),
34     mType(Fake),
35     mRouteSet(),
36 jason 2611 mLocalContact(),
37 jason 2577 mLocalCSeq(0),
38     mRemoteCSeq(0),
39 derek 2981 mRemoteTarget(),
40 derek 2995 mLocalNameAddr(),
41     mRemoteNameAddr(),
42     mCallId(msg.header(h_CallID)),
43 mfroman 6495 mDefaultSubExpiration(0),
44 derek 3308 mAppDialog(0),
45 sgodin 5265 mDestroying(false),
46     mReUseDialogSet(false)
47 jason 2577 {
48 davidb 2603 assert(msg.isExternal());
49 jason 2577
50 jason 4010 assert(msg.header(h_CSeq).method() != MESSAGE);
51 derek 3750 assert(msg.header(h_CSeq).method() != REGISTER);
52     assert(msg.header(h_CSeq).method() != PUBLISH);
53    
54 derek 4062 mNetworkAssociation.setDum(&dum);
55    
56 jason 2577 if (msg.isRequest()) // UAS
57     {
58     const SipMessage& request = msg;
59    
60     switch (request.header(h_CSeq).method())
61     {
62 jason 3433 case INVITE:
63 jason 2611 mType = Invitation;
64     break;
65 jason 4010
66 jason 3433 case SUBSCRIBE:
67     case REFER:
68     case NOTIFY:
69 derek 3041 //!dcm! -- event header check
70 jason 2611 mType = Subscription;
71     break;
72    
73     default:
74     mType = Fake;
75     }
76 derek 3024 if (request.exists(h_RecordRoutes))
77     {
78 sgodin 6902 mRouteSet = request.header(h_RecordRoutes);
79 derek 3024 }
80 jason 2611
81     switch (request.header(h_CSeq).method())
82     {
83 jason 3433 case INVITE:
84     case SUBSCRIBE:
85     case REFER:
86     case NOTIFY:
87 jason 4010 DebugLog ( << "UAS dialog ID creation, DS: " << ds.getId());
88 derek 6410 mId = DialogId(ds.getId(),
89     request.header(h_From).exists(p_tag) ? request.header(h_From).param(p_tag) : Data::Empty);
90    
91 derek 2995 mRemoteNameAddr = request.header(h_From);
92     mLocalNameAddr = request.header(h_To);
93     mLocalNameAddr.param(p_tag) = mId.getLocalTag();
94 jason 2577 if (request.exists(h_Contacts) && request.header(h_Contacts).size() == 1)
95     {
96 davidb 2603 const NameAddr& contact = request.header(h_Contacts).front();
97 jason 2577 if (isEqualNoCase(contact.uri().scheme(), Symbols::Sips) ||
98     isEqualNoCase(contact.uri().scheme(), Symbols::Sip))
99     {
100     mRemoteTarget = contact;
101 bcampen 8122
102     if (!mDialogSet.getUserProfile()->isAnonymous() && mDialogSet.getUserProfile()->hasPublicGruu())
103 moetje 6160 {
104 bcampen 8122 mLocalContact.uri() = mDialogSet.getUserProfile()->getPublicGruu();
105 moetje 6160 }
106 bcampen 8122 else if(mDialogSet.getUserProfile()->isAnonymous() && mDialogSet.getUserProfile()->hasTempGruu())
107     {
108     mLocalContact.uri() = mDialogSet.getUserProfile()->getTempGruu();
109     }
110     else
111     {
112     mLocalContact = NameAddr(request.header(h_RequestLine).uri()); // update later when send a request
113     if (mDialogSet.getUserProfile()->hasOverrideHostAndPort())
114     {
115     mLocalContact.uri().host() = mDialogSet.getUserProfile()->getOverrideHostAndPort().host();
116     mLocalContact.uri().port() = mDialogSet.getUserProfile()->getOverrideHostAndPort().port();
117     }
118     }
119 jason 2577 }
120     else
121     {
122 jason 3433 InfoLog(<< "Got an INVITE or SUBSCRIBE with invalid scheme");
123 jason 3483 InfoLog(<< request);
124 jason 2611 throw Exception("Invalid scheme in request", __FILE__, __LINE__);
125 jason 2577 }
126     }
127     else
128     {
129 jason 3433 InfoLog (<< "Got an INVITE or SUBSCRIBE that doesn't have exactly one contact");
130 jason 3483 InfoLog (<< request);
131 jason 2611 throw Exception("Too many (or no contact) contacts in request", __FILE__, __LINE__);
132 jason 2577 }
133     break;
134 davidb 2603 default:
135     break;
136 jason 2577 }
137 jason 4010
138 jason 2577 mRemoteCSeq = request.header(h_CSeq).sequence();
139 jason 2611 mLocalCSeq = 1;
140 derek 2995
141 jason 4010 DebugLog ( << "************** Created Dialog as UAS **************" );
142     DebugLog ( << "mRemoteNameAddr: " << mRemoteNameAddr );
143     DebugLog ( << "mLocalNameAddr: " << mLocalNameAddr );
144 jason 3156 DebugLog ( << "mLocalContact: " << mLocalContact );
145     DebugLog ( << "mRemoteTarget: " << mRemoteTarget );
146 jason 2577 }
147     else if (msg.isResponse())
148     {
149 jason 4010 mId = DialogId(msg);
150 jason 2577 const SipMessage& response = msg;
151 derek 2995 mRemoteNameAddr = response.header(h_To);
152     mLocalNameAddr = response.header(h_From);
153 jason 2611
154     switch (msg.header(h_CSeq).method())
155     {
156 jason 3433 case INVITE:
157 jason 2611 mType = Invitation;
158     break;
159 jason 4010
160 jason 3433 case SUBSCRIBE:
161     case REFER:
162 jason 2611 mType = Subscription;
163     break;
164    
165     default:
166     mType = Fake;
167     }
168    
169 jason 2577 if (response.exists(h_RecordRoutes))
170     {
171     mRouteSet = response.header(h_RecordRoutes).reverse();
172     }
173    
174     switch (response.header(h_CSeq).method())
175     {
176 jason 3433 case INVITE:
177     case SUBSCRIBE:
178     case REFER:
179 derek 2983 if (response.header(h_StatusLine).statusCode() > 100 &&
180     response.header(h_StatusLine).statusCode() < 300)
181 jason 2577 {
182 jason 4010
183 derek 2983 if (response.exists(h_Contacts) && response.header(h_Contacts).size() == 1)
184 jason 2577 {
185 derek 2983 const NameAddr& contact = response.header(h_Contacts).front();
186     if (isEqualNoCase(contact.uri().scheme(), Symbols::Sips) ||
187 sgodin 6873 isEqualNoCase(contact.uri().scheme(), Symbols::Sip))
188 derek 2983 {
189 jmatthewsr 6872 BaseCreator* creator = mDialogSet.getCreator();
190    
191 sgodin 6873 if( 0 == creator )
192     {
193     ErrLog(<< "BaseCreator is null for DialogSet");
194     ErrLog(<< response);
195     throw Exception("BaseCreator is null for DialogSet", __FILE__, __LINE__);
196     }
197    
198     SharedPtr<SipMessage> lastRequest(creator->getLastRequest());
199    
200 sgodin 6888 if( 0 == lastRequest.get() ||
201 sgodin 6873 !lastRequest->exists(h_Contacts) ||
202     lastRequest->header(h_Contacts).empty())
203     {
204     InfoLog(<< "lastRequest does not contain a valid contact");
205     InfoLog(<< response);
206     throw Exception("lastRequest does not contain a valid contact.", __FILE__, __LINE__);
207     }
208 daniel 5757 mLocalContact = creator->getLastRequest()->header(h_Contacts).front();
209 derek 2983 mRemoteTarget = contact;
210     }
211     else
212     {
213 jason 3433 InfoLog (<< "Got an INVITE or SUBSCRIBE with invalid scheme");
214 derek 2983 DebugLog (<< response);
215     throw Exception("Bad scheme in contact in response", __FILE__, __LINE__);
216     }
217 jason 2577 }
218     else
219     {
220 jason 3433 InfoLog (<< "Got an INVITE or SUBSCRIBE that doesn't have exactly one contact");
221 jason 2577 DebugLog (<< response);
222 derek 2983 throw Exception("Too many contacts (or no contact) in response", __FILE__, __LINE__);
223 jason 2577 }
224 derek 2983 break;
225     default:
226     break;
227 jason 2577 }
228     }
229 jason 4010
230 jason 2577 mLocalCSeq = response.header(h_CSeq).sequence();
231 jason 2611 mRemoteCSeq = 0;
232 jason 4010 DebugLog ( << "************** Created Dialog as UAC **************" );
233     DebugLog ( << "mRemoteNameAddr: " << mRemoteNameAddr );
234     DebugLog ( << "mLocalNameAddr: " << mLocalNameAddr );
235 jason 3156 DebugLog ( << "mLocalContact: " << mLocalContact );
236     DebugLog ( << "mRemoteTarget: " << mRemoteTarget );
237 jason 2577 }
238 derek 2867 mDialogSet.addDialog(this);
239 derek 3081 DebugLog ( <<"Dialog::Dialog " << mId);
240 jason 2577 }
241    
242 jason 2868 Dialog::~Dialog()
243     {
244 derek 3081 DebugLog ( <<"Dialog::~Dialog() ");
245 jason 4010
246 derek 2981 mDestroying = true;
247 derek 3089
248     while (!mClientSubscriptions.empty())
249 derek 2981 {
250 derek 3089 delete *mClientSubscriptions.begin();
251 derek 2981 }
252 derek 3089
253     while (!mServerSubscriptions.empty())
254 derek 3041 {
255 derek 3089 delete *mServerSubscriptions.begin();
256 derek 3041 }
257 derek 3089
258 derek 2981 delete mInviteSession;
259 jason 2884 mDialogSet.mDialogs.erase(this->getId());
260 derek 2976 delete mAppDialog;
261 sgodin 5265 if(!mReUseDialogSet)
262     {
263     mDialogSet.possiblyDie();
264     }
265 jason 2868 }
266    
267 cktam 4408 const DialogId&
268 jason 2588 Dialog::getId() const
269     {
270     return mId;
271     }
272    
273 bcampen 8200 const NameAddr&
274     Dialog::getLocalNameAddr() const
275     {
276     return mLocalNameAddr;
277     }
278    
279     const NameAddr&
280     Dialog::getLocalContact() const
281     {
282     return mLocalContact;
283     }
284    
285     const NameAddr&
286     Dialog::getRemoteNameAddr() const
287     {
288     return mRemoteNameAddr;
289     }
290    
291     const NameAddr&
292     Dialog::getRemoteTarget() const
293     {
294     return mRemoteTarget;
295     }
296    
297     const NameAddrs&
298     Dialog::getRouteSet() const
299     {
300     return mRouteSet;
301     }
302    
303 davidb 2604 void
304 jason 4010 Dialog::cancel()
305     {
306     assert(mType == Invitation);
307     ClientInviteSession* uac = dynamic_cast<ClientInviteSession*>(mInviteSession);
308     assert (uac);
309     uac->cancel();
310     }
311    
312     void
313 derek 3716 Dialog::end()
314 jason 2891 {
315 derek 3006 if (mInviteSession)
316     {
317 jason 3425 mInviteSession->end();
318 derek 3006 }
319 sgodin 7116
320     // End Subscriptions
321 jmatthewsr 7610 // !jrm! WARN ClientSubscription and ServerSubscription have access to this dialog and will remove themselves
322     // from the m<client|server>Subscriptions collections in the call to end().
323     for (list<ClientSubscription*>::iterator it(mClientSubscriptions.begin());
324     it != mClientSubscriptions.end();)
325 derek 3006 {
326 jmatthewsr 7610 ClientSubscription* c = *it;
327     it++;
328     c->end();
329 derek 3006 }
330 sgodin 7116
331 jmatthewsr 7610 for (list<ServerSubscription*>::iterator it2(mServerSubscriptions.begin());
332     it2 != mServerSubscriptions.end();)
333 sgodin 7116 {
334 jmatthewsr 7610 ServerSubscription* s = *it2;
335     it2++;
336     s->end();
337 sgodin 7116 }
338 jason 2891 }
339    
340     void
341 jason 3934 Dialog::handleTargetRefresh(const SipMessage& msg)
342     {
343     switch(msg.header(h_CSeq).method())
344     {
345     case INVITE:
346     case UPDATE:
347     if (msg.isRequest() || (msg.isResponse() && msg.header(h_StatusLine).statusCode()/100 == 2))
348     {
349     //?dcm? modify local target; 12.2.2 of 3261 implies that the remote
350     //target is immediately modified. Should we wait until a 2xx class
351     //reponse is sent to a re-invite(easy when all send requests go
352     //through Dialog)
353     if (msg.exists(h_Contacts))
354     {
355     //.dcm. replace or check then replace
356     mRemoteTarget = msg.header(h_Contacts).front();
357     }
358     }
359     break;
360     default:
361     return;
362     }
363     }
364    
365     void
366 davidb 2604 Dialog::dispatch(const SipMessage& msg)
367     {
368 jason 4010 // !jf! Should be checking for messages with out of order CSeq and rejecting
369    
370 jason 3156 DebugLog ( << "Dialog::dispatch: " << msg.brief());
371 daniel 5599
372 sgodin 6093 if(msg.isExternal())
373 sgodin 4113 {
374 sgodin 6093 const Data& receivedTransport = msg.header(h_Vias).front().transport();
375 daniel 5599 int keepAliveTime = 0;
376 sgodin 6093 if(receivedTransport == Symbols::TCP ||
377     receivedTransport == Symbols::TLS ||
378     receivedTransport == Symbols::SCTP)
379 daniel 5599 {
380     keepAliveTime = mDialogSet.getUserProfile()->getKeepAliveTimeForStream();
381     }
382     else
383     {
384     keepAliveTime = mDialogSet.getUserProfile()->getKeepAliveTimeForDatagram();
385     }
386    
387     if(keepAliveTime > 0)
388     {
389     mNetworkAssociation.update(msg, keepAliveTime);
390     }
391 sgodin 4113 }
392 daniel 5599
393 jason 3934 handleTargetRefresh(msg);
394 jason 2614 if (msg.isRequest())
395 jason 2583 {
396 jason 2614 const SipMessage& request = msg;
397     switch (request.header(h_CSeq).method())
398 jason 4010 {
399 jason 3433 case INVITE: // new INVITE
400 jason 2614 if (mInviteSession == 0)
401     {
402 jason 3156 DebugLog ( << "Dialog::dispatch -- Created new server invite session" << msg.brief());
403 jason 2885 mInviteSession = makeServerInviteSession(request);
404 jason 2614 }
405     mInviteSession->dispatch(request);
406     break;
407 jason 3433 //refactor, send bad request for BYE, INFO, CANCEL?
408     case BYE:
409 derek 2997 if (mInviteSession == 0)
410     {
411 jason 3433 InfoLog ( << "Spurious BYE" );
412 jason 4010 return;
413 derek 2997 }
414     else
415     {
416     mInviteSession->dispatch(request);
417     }
418     break;
419 jason 4010 case UPDATE:
420     if (mInviteSession == 0)
421     {
422     InfoLog ( << "Spurious UPDATE" );
423     return;
424     }
425     else
426     {
427     mInviteSession->dispatch(request);
428     }
429     break;
430 jason 3433 case INFO:
431 derek 3255 if (mInviteSession == 0)
432     {
433     InfoLog ( << "Spurious INFO" );
434 jason 4010 return;
435 derek 3255 }
436     else
437     {
438     mInviteSession->dispatch(request);
439     }
440     break;
441 sgodin 5265 case MESSAGE:
442     if (mInviteSession == 0)
443     {
444     InfoLog ( << "Spurious MESSAGE" );
445     return;
446     }
447     else
448     {
449     mInviteSession->dispatch(request);
450     }
451     break;
452 jason 3433 case ACK:
453     case CANCEL:
454 jason 2614 if (mInviteSession == 0)
455     {
456 jason 3433 InfoLog (<< "Drop stray ACK or CANCEL in dialog on the floor");
457 jason 2614 DebugLog (<< request);
458     }
459     else
460     {
461     mInviteSession->dispatch(request);
462     }
463     break;
464 jason 3433 case SUBSCRIBE:
465 derek 3041 {
466     ServerSubscription* server = findMatchingServerSub(request);
467     if (server)
468 jason 2614 {
469 derek 3041 server->dispatch(request);
470 jason 2614 }
471 derek 3041 else
472     {
473 sgodin 3493 if (request.exists(h_Event) && request.header(h_Event).value() == "refer")
474 derek 3101 {
475     InfoLog (<< "Received a subscribe to a non-existent refer subscription: " << request.brief());
476     SipMessage failure;
477     makeResponse(failure, request, 403);
478     mDum.sendResponse(failure);
479     return;
480     }
481     else
482     {
483 daniel 5738 if (mDum.checkEventPackage(request))
484     {
485     server = makeServerSubscription(request);
486     mServerSubscriptions.push_back(server);
487     server->dispatch(request);
488     }
489 derek 3101 }
490 derek 3041 }
491     }
492     break;
493 jason 3433 case REFER:
494 derek 3101 {
495 daniel 6126 // if (mInviteSession == 0)
496     // {
497     // InfoLog (<< "Received an in dialog refer in a non-invite dialog: " << request.brief());
498     // SipMessage failure;
499     // makeResponse(failure, request, 603);
500     // mDum.sendResponse(failure);
501     // return;
502     // }
503     // else
504    
505     if (!request.exists(h_ReferTo))
506 derek 3101 {
507     InfoLog (<< "Received refer w/out a Refer-To: " << request.brief());
508     SipMessage failure;
509     makeResponse(failure, request, 400);
510     mDum.sendResponse(failure);
511     return;
512     }
513     else
514     {
515 daniel 5830 if (request.exists(h_ReferSub) && request.header(h_ReferSub).value()=="false")
516 derek 3101 {
517 daniel 6126 assert(mInviteSession);
518 daniel 5830 mInviteSession->referNoSub(msg);
519 derek 3101 }
520     else
521     {
522 daniel 5830 ServerSubscription* server = findMatchingServerSub(request);
523     ServerSubscriptionHandle serverHandle;
524     if (server)
525     {
526     serverHandle = server->getHandle();
527     server->dispatch(request);
528     }
529     else
530     {
531     server = makeServerSubscription(request);
532     mServerSubscriptions.push_back(server);
533     serverHandle = server->getHandle();
534     server->dispatch(request);
535     }
536 daniel 6126
537     if (mInviteSession)
538     {
539     mDum.mInviteSessionHandler->onRefer(mInviteSession->getSessionHandle(), serverHandle, msg);
540     }
541    
542 derek 3101 }
543     }
544     }
545     break;
546 jason 3433 case NOTIFY:
547 jason 2614 {
548 sgodin 6925 ClientSubscription* client = findMatchingClientSub(request);
549     if (client)
550 jason 2612 {
551 sgodin 6925 client->dispatch(request);
552 jason 2612 }
553     else
554     {
555 sgodin 6925 BaseCreator* creator = mDialogSet.getCreator();
556     if (creator && (creator->getLastRequest()->header(h_RequestLine).method() == SUBSCRIBE ||
557     creator->getLastRequest()->header(h_RequestLine).method() == REFER))
558     {
559     DebugLog (<< "Making subscription (from creator) request: " << *creator->getLastRequest());
560     ClientSubscription* sub = makeClientSubscription(*creator->getLastRequest());
561     mClientSubscriptions.push_back(sub);
562     sub->dispatch(request);
563     }
564     else
565     {
566     if (mInviteSession != 0 && (!msg.exists(h_Event) || msg.header(h_Event).value() == "refer") &&
567     mDum.getClientSubscriptionHandler("refer")!=0)
568     {
569     DebugLog (<< "Making subscription from NOTIFY: " << msg);
570     ClientSubscription* sub = makeClientSubscription(msg);
571     mClientSubscriptions.push_back(sub);
572     ClientSubscriptionHandle client = sub->getHandle();
573     mDum.mInviteSessionHandler->onReferAccepted(mInviteSession->getSessionHandle(), client, msg);
574     sub->dispatch(request);
575     }
576     else
577     {
578     SharedPtr<SipMessage> response(new SipMessage);
579     makeResponse(*response, msg, 406);
580     send(response);
581     }
582     }
583     }
584 jason 2614 }
585 sgodin 6925 break;
586 derek 3089 default:
587     assert(0);
588     return;
589 jason 2583 }
590 jason 2614 }
591     else if (msg.isResponse())
592     {
593 jason 4010 // !jf! There is a substantial change in how this works in teltel-branch
594     // from how it worked in main branch pre merge.
595     // If the response doesn't match a cseq for a request I've sent, ignore
596     // the response
597 jmatthewsr 8411 {//scope 'r' as it is invalidated below
598     RequestMap::iterator r = mRequests.find(msg.header(h_CSeq).sequence());
599     if (r != mRequests.end())
600     {
601     bool handledByAuth = false;
602     if (mDum.mClientAuthManager.get() &&
603     mDum.mClientAuthManager->handle(*mDialogSet.getUserProfile(), *r->second, msg))
604     {
605     InfoLog( << "about to re-send request with digest credentials" << r->second->brief());
606 jason 4010
607 jmatthewsr 8411 assert (r->second->isRequest());
608 jason 4010
609 jmatthewsr 8411 mLocalCSeq++;
610     send(r->second);
611     handledByAuth = true;
612     }
613     mRequests.erase(r);
614     if (handledByAuth) return;
615 derek 3089 }
616     }
617 derek 5642
618 jason 2614 const SipMessage& response = msg;
619 derek 3112 int code = response.header(h_StatusLine).statusCode();
620 sgodin 6902 // If this is a 200 response to the initial request, then store the routeset (if present)
621 sgodin 6904 BaseCreator* creator = mDialogSet.getCreator();
622 sgodin 6993 if (creator && (creator->getLastRequest()->header(h_CSeq) == response.header(h_CSeq)) && code >=200 && code < 300)
623 derek 3112 {
624     if (response.exists(h_RecordRoutes))
625     {
626     mRouteSet = response.header(h_RecordRoutes).reverse();
627     }
628 sgodin 7278 else
629     {
630     // Ensure that if the route-set in the 200 is empty, then we overwrite any existing route-sets
631     mRouteSet.clear();
632     }
633 derek 3112 }
634 jason 4010
635 jason 2885 // !jf! should this only be for 2xx responses? !jf! Propose no as an
636     // answer !dcm! what is he on?
637 jason 2614 switch (response.header(h_CSeq).method())
638 jason 2583 {
639 jason 3433 case INVITE:
640 jason 2614 if (mInviteSession == 0)
641     {
642 jason 3156 DebugLog ( << "Dialog::dispatch -- Created new client invite session" << msg.brief());
643 derek 2992
644 jason 2885 mInviteSession = makeClientInviteSession(response);
645 jmatthewsr 8411 if (mInviteSession)
646     {
647     mInviteSession->dispatch(response);
648     }
649     else
650     {
651     ErrLog( << "Dialog::dispatch -- Unable to create invite session from response" << msg.brief());
652     }
653 jason 2614 }
654     else
655     {
656     mInviteSession->dispatch(response);
657     }
658     break;
659 jason 3433 case BYE:
660     case ACK:
661     case CANCEL:
662     case INFO:
663 sgodin 5265 case MESSAGE:
664 jason 4010 case UPDATE:
665     if (mInviteSession)
666 jason 2614 {
667     mInviteSession->dispatch(response);
668     }
669     // else drop on the floor
670 jason 4010 break;
671    
672     case REFER:
673 sgodin 4021 if(mInviteSession)
674 jason 2612 {
675 sgodin 4021 if (code >= 300)
676     {
677     mDum.mInviteSessionHandler->onReferRejected(mInviteSession->getSessionHandle(), msg);
678     }
679 daniel 5830 else
680     {
681 daniel 6074 //!dys! the OR condition below is not draft compliant.
682     if (!mInviteSession->mReferSub &&
683     ((msg.exists(h_ReferSub) && msg.header(h_ReferSub).value()=="false") ||
684     !msg.exists(h_ReferSub)))
685 daniel 5830 {
686 daniel 6074 DebugLog(<< "refer accepted with norefersub");
687 daniel 5830 mDum.mInviteSessionHandler->onReferAccepted(mInviteSession->getSessionHandle(), ClientSubscriptionHandle::NotValid(), msg);
688     }
689     // else no need for action - first Notify will cause onReferAccepted to be called
690     }
691 sgodin 7565 mInviteSession->nitComplete();
692 daniel 6126 break;
693 jason 4010 }
694 daniel 6126 // fall through, out of dialog refer was sent.
695 mfroman 6495
696 jason 3433 case SUBSCRIBE:
697 derek 3058 {
698     int code = response.header(h_StatusLine).statusCode();
699 daniel 5767 ClientSubscription* client = findMatchingClientSub(response);
700     if (client)
701 derek 3058 {
702 daniel 5767 client->dispatch(response);
703     }
704     else if (code < 300)
705     {
706 mfroman 6495 /*
707     we're capturing the value from the expires header off
708     the 2xx because the ClientSubscription is only created
709     after receiving the NOTIFY that comes (usually) after
710     this 2xx. We really should be creating the
711     ClientSubscription at either the 2xx or the NOTIFY
712     whichever arrives first. .mjf.
713     Note: we're capturing a duration here (not the
714     absolute time because all the inputs to
715     ClientSubscription desling with the expiration are expecting
716     duration type values from the headers. .mjf.
717     */
718 bcampen 7466 if(response.exists(h_Expires))
719     {
720     mDefaultSubExpiration = response.header(h_Expires).value();
721     }
722     else
723     {
724     //?dcm? defaults to 3600 in ClientSubscription if no expires value
725     //is provided anywhere...should we assume the value from the
726     //sub in the basecreator if it exists?
727     mDefaultSubExpiration = 0;
728     }
729 derek 3058 return;
730 jason 4010 }
731 derek 3058 else
732     {
733 daniel 5767 //!dcm! -- can't subscribe in an existing Dialog, this is all
734     //a bit of a hack; currently, spurious failure messages may cause callbacks
735     BaseCreator* creator = mDialogSet.getCreator();
736     if (!creator || !creator->getLastRequest()->exists(h_Event))
737 derek 3058 {
738 daniel 5767 return;
739 derek 3058 }
740 derek 3082 else
741     {
742 daniel 5767 ClientSubscriptionHandler* handler =
743     mDum.getClientSubscriptionHandler(creator->getLastRequest()->header(h_Event).value());
744     if (handler)
745 derek 3274 {
746 daniel 5767 ClientSubscription* sub = makeClientSubscription(*creator->getLastRequest());
747     mClientSubscriptions.push_back(sub);
748     sub->dispatch(response);
749 derek 3274 }
750 derek 3082 }
751 jason 2614 }
752 daniel 5767
753 jason 2614 }
754 derek 3058 break;
755 jason 3433 case NOTIFY:
756 derek 3101 {
757 derek 3274 //2xx responses are treated as retransmission quenchers(handled by
758     //the stack). Failures are dispatched to all ServerSubsscriptions,
759     //which may not be correct.
760    
761     int code = msg.header(h_StatusLine).statusCode();
762     if (code >= 300)
763 derek 3101 {
764 derek 3274 //!dcm! -- ick, write guard
765 jason 4010 mDestroying = true;
766 derek 3274 for (list<ServerSubscription*>::iterator it = mServerSubscriptions.begin();
767     it != mServerSubscriptions.end(); )
768     {
769     ServerSubscription* s = *it;
770     it++;
771     s->dispatch(msg);
772     }
773     mDestroying = false;
774     possiblyDie();
775 derek 3101 }
776 derek 3274 // ServerSubscription* server = findMatchingServerSub(response);
777     // if (server)
778     // {
779     // server->dispatch(response);
780     // }
781 derek 3101 }
782 jason 4010 break;
783 derek 3101 default:
784     assert(0);
785     return;
786 jason 2583 }
787 jason 4010
788     #if 0 // merged from head back to teltel-branch
789     if (msg.header(h_StatusLine).statusCode() >= 400
790 derek 3750 && Helper::determineFailureMessageEffect(msg) == Helper::DialogTermination)
791     {
792     //kill all usages
793 jason 4010 mDestroying = true;
794    
795 derek 3750 for (list<ServerSubscription*>::iterator it = mServerSubscriptions.begin();
796     it != mServerSubscriptions.end(); )
797     {
798     ServerSubscription* s = *it;
799     it++;
800     s->dialogDestroyed(msg);
801 jason 4010 }
802 derek 3750
803     for (list<ClientSubscription*>::iterator it = mClientSubscriptions.begin();
804     it != mClientSubscriptions.end(); )
805     {
806     ClientSubscription* s = *it;
807     it++;
808     s->dialogDestroyed(msg);
809     }
810     if (mInviteSession)
811     {
812     mInviteSession->dialogDestroyed(msg);
813     }
814     mDestroying = false;
815     possiblyDie(); //should aways result in destruction of this
816 jason 4010 return;
817     }
818 derek 3750 #endif
819 jason 2583 }
820 jason 2578 }
821    
822 jason 4010 ServerSubscription*
823 derek 3041 Dialog::findMatchingServerSub(const SipMessage& msg)
824     {
825 jason 4010 for (std::list<ServerSubscription*>::iterator i=mServerSubscriptions.begin();
826 derek 3041 i != mServerSubscriptions.end(); ++i)
827     {
828     if ((*i)->matches(msg))
829     {
830     return *i;
831     }
832     }
833     return 0;
834     }
835 jason 2612
836 jason 4010 ClientSubscription*
837 jason 2612 Dialog::findMatchingClientSub(const SipMessage& msg)
838 jason 2583 {
839 jason 4010 for (std::list<ClientSubscription*>::iterator i=mClientSubscriptions.begin();
840 jason 2612 i != mClientSubscriptions.end(); ++i)
841 jason 2583 {
842 jason 2614 if ((*i)->matches(msg))
843 jason 2612 {
844 jason 2614 return *i;
845 jason 2612 }
846     }
847 jason 2614 return 0;
848 jason 2612 }
849 jason 2585
850 jason 2941 InviteSessionHandle
851 derek 3041 Dialog::getInviteSession()
852 ken 2520 {
853 davidb 2603 if (mInviteSession)
854     {
855     return mInviteSession->getSessionHandle();
856     }
857     else
858     {
859 derek 3041 return InviteSessionHandle::NotValid();
860 davidb 2603 }
861 ken 2520 }
862    
863 jason 4010 std::vector<ClientSubscriptionHandle>
864 derek 3041 Dialog::findClientSubscriptions(const Data& event)
865 ken 2520 {
866 jason 2941 std::vector<ClientSubscriptionHandle> handles;
867 jason 4010
868 derek 2858 for (std::list<ClientSubscription*>::const_iterator i = mClientSubscriptions.begin();
869 davidb 2603 i != mClientSubscriptions.end(); ++i)
870     {
871 derek 3041 if ( (*i)->getEventType() == event)
872     {
873     handles.push_back((*i)->getHandle());
874     }
875     }
876     return handles;
877     }
878    
879 jason 4010 std::vector<ServerSubscriptionHandle>
880 derek 3041 Dialog::findServerSubscriptions(const Data& event)
881     {
882     std::vector<ServerSubscriptionHandle> handles;
883 jason 4010
884 derek 3041 for (std::list<ServerSubscription*>::const_iterator i = mServerSubscriptions.begin();
885     i != mServerSubscriptions.end(); ++i)
886     {
887     if ( (*i)->getEventType() == event)
888     {
889     handles.push_back((*i)->getHandle());
890     }
891     }
892     return handles;
893     }
894    
895 jason 4010 std::vector<ClientSubscriptionHandle>
896 derek 3041 Dialog::getClientSubscriptions()
897     {
898     std::vector<ClientSubscriptionHandle> handles;
899 jason 4010
900 derek 3041 for (std::list<ClientSubscription*>::const_iterator i = mClientSubscriptions.begin();
901     i != mClientSubscriptions.end(); ++i)
902     {
903 davidb 2603 handles.push_back((*i)->getHandle());
904     }
905    
906     return handles;
907 ken 2520 }
908    
909 jason 4010 std::vector<ServerSubscriptionHandle>
910 derek 3041 Dialog::getServerSubscriptions()
911     {
912     std::vector<ServerSubscriptionHandle> handles;
913 jason 4010
914 derek 3041 for (std::list<ServerSubscription*>::const_iterator i = mServerSubscriptions.begin();
915     i != mServerSubscriptions.end(); ++i)
916     {
917     handles.push_back((*i)->getHandle());
918     }
919    
920     return handles;
921     }
922    
923 jason 4010 void
924 derek 3179 Dialog::redirected(const SipMessage& msg)
925     {
926     //Established dialogs are not destroyed by a redirect
927     if (!mClientSubscriptions.empty() || !mServerSubscriptions.empty())
928     {
929     return;
930     }
931     if (mInviteSession)
932     {
933     ClientInviteSession* cInv = dynamic_cast<ClientInviteSession*>(mInviteSession);
934     if (cInv)
935     {
936 jason 4010 cInv->handleRedirect(msg);
937 sgodin 5265 mReUseDialogSet = true; // Set flag so that DialogSet will not be destroyed and new Request can use it
938 derek 3179 }
939     }
940     }
941 ken 2520
942 jason 2539 void
943 derek 2813 Dialog::makeRequest(SipMessage& request, MethodTypes method)
944     {
945     RequestLine rLine(method);
946    
947 derek 2817 rLine.uri() = mRemoteTarget.uri();
948 jason 4010
949 derek 2813 request.header(h_RequestLine) = rLine;
950 jason 4010 request.header(h_To) = mRemoteNameAddr;
951 derek 2995 // request.header(h_To).param(p_tag) = mId.getRemoteTag();
952 jason 4010 request.header(h_From) = mLocalNameAddr;
953     // request.header(h_From).param(p_tag) = mId.getLocalTag();
954 derek 2813
955     request.header(h_CallId) = mCallId;
956 derek 2997
957     request.remove(h_RecordRoutes); //!dcm! -- all of this is rather messy
958 sgodin 6198 request.remove(h_Replaces);
959 derek 2997
960 jason 4010 request.remove(h_Contacts);
961     request.header(h_Contacts).push_front(mLocalContact);
962    
963 derek 2813 request.header(h_CSeq).method() = method;
964     request.header(h_MaxForwards).value() = 70;
965    
966 derek 2961 //must keep old via for cancel
967 jason 3433 if (method != CANCEL)
968 derek 2961 {
969 derek 3024 request.header(h_Routes) = mRouteSet;
970 jason 4010 request.remove(h_Vias);
971 derek 2961 Via via;
972     via.param(p_branch); // will create the branch
973     request.header(h_Vias).push_front(via);
974     }
975     else
976     {
977     assert(request.exists(h_Vias));
978     }
979 sgodin 3392
980 jason 3433 //don't increment CSeq for ACK or CANCEL
981     if (method != ACK && method != CANCEL)
982 derek 2965 {
983     request.header(h_CSeq).sequence() = ++mLocalCSeq;
984     }
985 sgodin 3383 else
986     {
987 jason 3433 // ACK and cancel have a minimal header set
988 sgodin 3383 request.remove(h_Accepts);
989     request.remove(h_AcceptEncodings);
990     request.remove(h_AcceptLanguages);
991     request.remove(h_Allows);
992     request.remove(h_Requires);
993     request.remove(h_ProxyRequires);
994     request.remove(h_Supporteds);
995 sgodin 5586 // request.header(h_CSeq).sequence() = ?; // Caller should provide original request, or modify CSeq to proper value after calling this method
996 sgodin 3383 }
997 sgodin 3392
998 jason 3433 // If method is INVITE then advertise required headers
999 jason 4010 if(method == INVITE || method == UPDATE)
1000 sgodin 3392 {
1001 jason 4010 if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::Allow)) request.header(h_Allows) = mDum.getMasterProfile()->getAllowedMethods();
1002     if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::AcceptEncoding)) request.header(h_AcceptEncodings) = mDum.getMasterProfile()->getSupportedEncodings();
1003     if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::AcceptLanguage)) request.header(h_AcceptLanguages) = mDum.getMasterProfile()->getSupportedLanguages();
1004 jason 5459 if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::AllowEvents)) request.header(h_AllowEvents) = mDum.getMasterProfile()->getAllowedEvents();
1005 jason 4010 if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::Supported)) request.header(h_Supporteds) = mDum.getMasterProfile()->getSupportedOptionTags();
1006 sgodin 3392 }
1007    
1008 jason 6075 if (mDialogSet.mUserProfile->isAnonymous())
1009     {
1010 sgodin 8353 request.header(h_Privacys).push_back(PrivacyCategory(Symbols::id));
1011 jason 6075 }
1012    
1013 jmatthewsr 6643 DebugLog ( << "Dialog::makeRequest: " << std::endl << std::endl << request );
1014 derek 2813 }
1015    
1016 derek 3138
1017 jason 4010 void
1018 jason 2887 Dialog::makeResponse(SipMessage& response, const SipMessage& request, int code)
1019 derek 2813 {
1020     assert( code >= 100 );
1021 jason 4010 response.remove(h_Contacts);
1022 derek 2992 if (code < 300 && code > 100)
1023 derek 2813 {
1024     assert(request.isRequest());
1025 jason 3433 assert(request.header(h_RequestLine).getMethod() == INVITE ||
1026     request.header(h_RequestLine).getMethod() == SUBSCRIBE ||
1027     request.header(h_RequestLine).getMethod() == BYE ||
1028     request.header(h_RequestLine).getMethod() == CANCEL ||
1029     request.header(h_RequestLine).getMethod() == REFER ||
1030     request.header(h_RequestLine).getMethod() == MESSAGE ||
1031     request.header(h_RequestLine).getMethod() == NOTIFY ||
1032     request.header(h_RequestLine).getMethod() == INFO ||
1033 jason 4010 request.header(h_RequestLine).getMethod() == OPTIONS ||
1034     request.header(h_RequestLine).getMethod() == UPDATE
1035 derek 3082 );
1036 jason 4010
1037 derek 3289 // assert (request.header(h_RequestLine).getMethod() == CANCEL || // Contact header is not required for Requests that do not form a dialog
1038 jason 3433 // request.header(h_RequestLine).getMethod() == BYE ||
1039 derek 3289 // request.header(h_Contacts).size() == 1);
1040 derek 2992 Helper::makeResponse(response, request, code, mLocalContact);
1041 derek 2936 response.header(h_To).param(p_tag) = mId.getLocalTag();
1042 jason 4010
1043     if((request.header(h_RequestLine).getMethod() == INVITE ||
1044     request.header(h_RequestLine).getMethod() == UPDATE)
1045     && code >= 200 && code < 300)
1046     {
1047     // Check if we should add our capabilites to the invite success response
1048 derek 7144 if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::Allow))
1049     {
1050     response.header(h_Allows) = mDum.getMasterProfile()->getAllowedMethods();
1051     }
1052     if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::AcceptEncoding))
1053     {
1054     response.header(h_AcceptEncodings) = mDum.getMasterProfile()->getSupportedEncodings();
1055     }
1056     if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::AcceptLanguage))
1057     {
1058     response.header(h_AcceptLanguages) = mDum.getMasterProfile()->getSupportedLanguages();
1059     }
1060     if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::AllowEvents))
1061     {
1062     response.header(h_AllowEvents) = mDum.getMasterProfile()->getAllowedEvents();
1063     }
1064     if(mDialogSet.getUserProfile()->isAdvertisedCapability(Headers::Supported))
1065     {
1066     response.header(h_Supporteds) = mDum.getMasterProfile()->getSupportedOptionTags();
1067     }
1068 jason 4010 }
1069 derek 2813 }
1070     else
1071     {
1072 derek 3274 Helper::makeResponse(response, request, code);
1073 derek 3006 response.header(h_To).param(p_tag) = mId.getLocalTag();
1074 derek 2813 }
1075 moetje 6159
1076 jmatthewsr 6643 DebugLog ( << "Dialog::makeResponse: " << std::endl << std::endl << response);
1077 derek 2813 }
1078    
1079 jason 2885
1080     ClientInviteSession*
1081     Dialog::makeClientInviteSession(const SipMessage& response)
1082     {
1083     InviteSessionCreator* creator = dynamic_cast<InviteSessionCreator*>(mDialogSet.getCreator());
1084 jmatthewsr 8411 if (!creator)
1085     {
1086     assert(0); // !jf! this maybe can assert by evil UAS
1087     return 0;
1088     }
1089 jason 2941 //return mDum.createAppClientInviteSession(*this, *creator);
1090 jason 4010 return new ClientInviteSession(mDum, *this, creator->getLastRequest(),
1091 daniel 5068 creator->getInitialOffer(), creator->getEncryptionLevel(), creator->getServerSubscription());
1092 jason 2885 }
1093    
1094    
1095    
1096     ClientSubscription*
1097 derek 3058 Dialog::makeClientSubscription(const SipMessage& request)
1098 jason 2885 {
1099 mfroman 6495 return new ClientSubscription(mDum, *this, request, mDefaultSubExpiration);
1100 jason 2885 }
1101    
1102    
1103     ServerInviteSession*
1104     Dialog::makeServerInviteSession(const SipMessage& request)
1105     {
1106 jason 2941 return new ServerInviteSession(mDum, *this, request);
1107 jason 2885 }
1108    
1109 jason 4010 ServerSubscription*
1110 jason 2885 Dialog::makeServerSubscription(const SipMessage& request)
1111     {
1112 jason 2941 return new ServerSubscription(mDum, *this, request);
1113 jason 2885 }
1114    
1115 davidb 2603 Dialog::Exception::Exception(const Data& msg, const Data& file, int line)
1116     : BaseException(msg, file, line)
1117 jason 2885 {
1118     }
1119 davidb 2603
1120 jason 4010
1121 jason 2853 void
1122 daniel 5757 Dialog::send(SharedPtr<SipMessage> msg)
1123 jason 2853 {
1124 daniel 5757 if (msg->isRequest() && msg->header(h_CSeq).method() != ACK)
1125 jason 4010 {
1126 daniel 5757 mRequests[msg->header(h_CSeq).sequence()] = msg;
1127 jason 4010 }
1128 daniel 5383 mDum.send(msg);
1129 jason 2853 }
1130    
1131 jason 4010 void
1132     Dialog::onForkAccepted()
1133 derek 2839 {
1134 jason 4010 ClientInviteSession* uac = dynamic_cast<ClientInviteSession*>(mInviteSession);
1135     if (uac)
1136     {
1137     uac->onForkAccepted();
1138     }
1139 derek 2839 }
1140    
1141 derek 2858 void Dialog::possiblyDie()
1142     {
1143 sgodin 6914 // !slg! Note: dialogs should really stick around for 32s, in order to ensure that all 2xx retransmissions get Ack'd, then BYE'd correctly
1144 derek 2981 if (!mDestroying)
1145 derek 2858 {
1146 derek 2981 if (mClientSubscriptions.empty() &&
1147 derek 3041 mServerSubscriptions.empty() &&
1148 derek 3089 !mInviteSession)
1149 derek 2981 {
1150 sgodin 5042 mDestroying = true;
1151 jason 4010 mDum.destroy(this);
1152 derek 2981 }
1153 derek 3308 }
1154     }
1155    
1156 jmatthewsr 8161 EncodeStream&
1157     resip::operator<<(EncodeStream& strm, const Dialog& dialog)
1158 jason 2884 {
1159 jason 4010 strm
1160     << "mClientSubscriptions("
1161     << dialog.mClientSubscriptions.size()
1162     << "), "
1163     << "mServerSubscriptions("
1164     << dialog.mServerSubscriptions.size()
1165     << ")";
1166 jason 2884 return strm;
1167     }
1168    
1169 derek 3716
1170 jason 4010 /* ====================================================================
1171     * The Vovida Software License, Version 1.0
1172     *
1173     * Copyright (c) 2000 Vovida Networks, Inc. All rights reserved.
1174     *
1175     * Redistribution and use in source and binary forms, with or without
1176     * modification, are permitted provided that the following conditions
1177     * are met:
1178     *
1179     * 1. Redistributions of source code must retain the above copyright
1180     * notice, this list of conditions and the following disclaimer.
1181     *
1182     * 2. Redistributions in binary form must reproduce the above copyright
1183     * notice, this list of conditions and the following disclaimer in
1184     * the documentation and/or other materials provided with the
1185     * distribution.
1186     *
1187     * 3. The names "VOCAL", "Vovida Open Communication Application Library",
1188     * and "Vovida Open Communication Application Library (VOCAL)" must
1189     * not be used to endorse or promote products derived from this
1190     * software without prior written permission. For written
1191     * permission, please contact vocal@vovida.org.
1192     *
1193     * 4. Products derived from this software may not be called "VOCAL", nor
1194     * may "VOCAL" appear in their name, without prior written
1195     * permission of Vovida Networks, Inc.
1196     *
1197     * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
1198     * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
1199     * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND
1200     * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA
1201     * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES
1202     * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
1203     * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
1204     * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
1205     * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
1206     * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
1207     * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
1208     * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
1209     * DAMAGE.
1210     *
1211     * ====================================================================
1212     *
1213     * This software consists of voluntary contributions made by Vovida
1214     * Networks, Inc. and many individuals on behalf of Vovida Networks,
1215     * Inc. For more information on Vovida Networks, Inc., please see
1216     * <http://www.vovida.org/>.
1217     *
1218     */
1219 derek 3716
1220 jason 5459
1221 sgodin 7565

Properties

Name Value
svn:eol-style native
svn:mime-type text/plain

webmaster AT resiprocate DOT org
ViewVC Help
Powered by ViewVC 1.1.27