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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3102 - (show annotations) (download)
Wed Jul 14 20:07:01 2004 UTC (15 years, 5 months ago) by sgodin
Original Path: main/sip/resiprocate/dum/Dialog.cxx
File size: 23789 byte(s)
Fix for Dialog::cancel with InviteSession
1 #include "resiprocate/Contents.hxx"
2 #include "resiprocate/Helper.hxx"
3 #include "resiprocate/SipMessage.hxx"
4 #include "resiprocate/dum/AppDialog.hxx"
5 #include "resiprocate/dum/BaseCreator.hxx"
6 #include "resiprocate/dum/ClientAuthManager.hxx"
7 #include "resiprocate/dum/ClientInviteSession.hxx"
8 #include "resiprocate/dum/ClientSubscription.hxx"
9 #include "resiprocate/dum/Dialog.hxx"
10 #include "resiprocate/dum/DialogUsageManager.hxx"
11 #include "resiprocate/dum/InviteSessionCreator.hxx"
12 #include "resiprocate/dum/InviteSessionHandler.hxx"
13 #include "resiprocate/dum/ServerInviteSession.hxx"
14 #include "resiprocate/dum/ServerSubscription.hxx"
15 #include "resiprocate/dum/SubscriptionHandler.hxx"
16 #include "resiprocate/dum/UsageUseException.hxx"
17 #include "resiprocate/os/Logger.hxx"
18
19 #define _CRTDBG_MAP_ALLOC
20 #include <stdlib.h>
21 #include <crtdbg.h>
22 #ifdef _DEBUG
23 #define new new( _NORMAL_BLOCK, __FILE__, __LINE__)
24 #endif // _DEBUG
25
26 #define RESIPROCATE_SUBSYSTEM Subsystem::DUM
27
28 using namespace resip;
29 using namespace std;
30
31 Dialog::Dialog(DialogUsageManager& dum, const SipMessage& msg, DialogSet& ds)
32 : mDum(dum),
33 mDialogSet(ds),
34 mId("INVALID", "INVALID", "INVALID"),
35 mClientSubscriptions(),
36 mServerSubscriptions(),
37 mInviteSession(0),
38 mType(Fake),
39 mRouteSet(),
40 mLocalContact(),
41 mLocalCSeq(0),
42 mRemoteCSeq(0),
43 mRemoteTarget(),
44 mLocalNameAddr(),
45 mRemoteNameAddr(),
46 mCallId(msg.header(h_CallID)),
47 mDestroying(false)
48 {
49 assert(msg.isExternal());
50
51
52 if (msg.isRequest()) // UAS
53 {
54 const SipMessage& request = msg;
55
56 switch (request.header(h_CSeq).method())
57 {
58 case INVITE:
59 mType = Invitation;
60 break;
61
62 case SUBSCRIBE:
63 case REFER:
64 case NOTIFY:
65 //!dcm! -- event header check
66 mType = Subscription;
67 break;
68
69 default:
70 mType = Fake;
71 }
72 if (request.exists(h_RecordRoutes))
73 {
74 mRouteSet = request.header(h_RecordRoutes); // !jf! is this right order
75 }
76
77 switch (request.header(h_CSeq).method())
78 {
79 case INVITE:
80 case SUBSCRIBE:
81 case REFER:
82 InfoLog ( << "UAS dialog ID creation, DS: " << ds.getId());
83 mId = DialogId(ds.getId(), request.header(h_From).param(p_tag));
84 mRemoteNameAddr = request.header(h_From);
85 mLocalNameAddr = request.header(h_To);
86 mLocalNameAddr.param(p_tag) = mId.getLocalTag();
87 if (request.exists(h_Contacts) && request.header(h_Contacts).size() == 1)
88 {
89 const NameAddr& contact = request.header(h_Contacts).front();
90 if (isEqualNoCase(contact.uri().scheme(), Symbols::Sips) ||
91 isEqualNoCase(contact.uri().scheme(), Symbols::Sip))
92 {
93 mLocalContact = NameAddr(request.header(h_RequestLine).uri()); // update later when send a request
94 mRemoteTarget = contact;
95 }
96 else
97 {
98 InfoLog(<< "Got an INVITE or SUBSCRIBE with invalid scheme");
99 DebugLog(<< request);
100 throw Exception("Invalid scheme in request", __FILE__, __LINE__);
101 }
102 }
103 else
104 {
105 InfoLog (<< "Got an INVITE or SUBSCRIBE that doesn't have exactly one contact");
106 DebugLog (<< request);
107 throw Exception("Too many (or no contact) contacts in request", __FILE__, __LINE__);
108 }
109 break;
110 default:
111 break;
112 }
113
114 mRemoteCSeq = request.header(h_CSeq).sequence();
115 mLocalCSeq = 1;
116
117 InfoLog ( << "************** Created Dialog as UAS **************" );
118 InfoLog ( << "mRemoteNameAddr: " << mRemoteNameAddr );
119 InfoLog ( << "mLocalNameAddr: " << mLocalNameAddr );
120 InfoLog ( << "mLocalContact: " << mLocalContact );
121 InfoLog ( << "mRemoteTarget: " << mRemoteTarget );
122 }
123 else if (msg.isResponse())
124 {
125 mId = DialogId(msg);
126 const SipMessage& response = msg;
127 mRemoteNameAddr = response.header(h_To);
128 mLocalNameAddr = response.header(h_From);
129
130 switch (msg.header(h_CSeq).method())
131 {
132 case INVITE:
133 mType = Invitation;
134 break;
135
136 case SUBSCRIBE:
137 case REFER:
138 mType = Subscription;
139 break;
140
141 default:
142 mType = Fake;
143 }
144
145 if (response.exists(h_RecordRoutes))
146 {
147 mRouteSet = response.header(h_RecordRoutes).reverse();
148 }
149
150 switch (response.header(h_CSeq).method())
151 {
152 case INVITE:
153 case SUBSCRIBE:
154 case REFER:
155 if (response.header(h_StatusLine).statusCode() > 100 &&
156 response.header(h_StatusLine).statusCode() < 300)
157 {
158
159 if (response.exists(h_Contacts) && response.header(h_Contacts).size() == 1)
160 {
161 const NameAddr& contact = response.header(h_Contacts).front();
162 if (isEqualNoCase(contact.uri().scheme(), Symbols::Sips) ||
163 isEqualNoCase(contact.uri().scheme(), Symbols::Sip))
164 {
165 BaseCreator* creator = mDum.findCreator(mId);
166 assert(creator);// !jf! throw or something here
167
168 mLocalContact = creator->getLastRequest().header(h_Contacts).front();
169 mRemoteTarget = contact;
170 }
171 else
172 {
173 InfoLog (<< "Got an INVITE or SUBSCRIBE with invalid scheme");
174 DebugLog (<< response);
175 throw Exception("Bad scheme in contact in response", __FILE__, __LINE__);
176 }
177 }
178 else
179 {
180 InfoLog (<< "Got an INVITE or SUBSCRIBE that doesn't have exactly one contact");
181 DebugLog (<< response);
182 throw Exception("Too many contacts (or no contact) in response", __FILE__, __LINE__);
183 }
184 break;
185 default:
186 break;
187 }
188 }
189
190 mLocalCSeq = response.header(h_CSeq).sequence();
191 mRemoteCSeq = 0;
192 InfoLog ( << "************** Created Dialog as UAC **************" );
193 InfoLog ( << "mRemoteNameAddr: " << mRemoteNameAddr );
194 InfoLog ( << "mLocalNameAddr: " << mLocalNameAddr );
195 InfoLog ( << "mLocalContact: " << mLocalContact );
196 InfoLog ( << "mRemoteTarget: " << mRemoteTarget );
197
198
199 }
200 mDialogSet.addDialog(this);
201 DebugLog ( <<"Dialog::Dialog " << mId);
202 }
203
204 Dialog::~Dialog()
205 {
206 DebugLog ( <<"Dialog::~Dialog() ");
207
208 mDestroying = true;
209
210 while (!mClientSubscriptions.empty())
211 {
212 delete *mClientSubscriptions.begin();
213 }
214
215 while (!mServerSubscriptions.empty())
216 {
217 delete *mServerSubscriptions.begin();
218 }
219
220 delete mInviteSession;
221
222 mDialogSet.mDialogs.erase(this->getId());
223 delete mAppDialog;
224 mDialogSet.possiblyDie();
225 }
226
227 DialogId
228 Dialog::getId() const
229 {
230 return mId;
231 }
232
233 void
234 Dialog::cancel()
235 {
236 if (mInviteSession)
237 {
238 mInviteSession->send(mInviteSession->end());
239 }
240 else
241 {
242 if (mDialogSet.getCreator())
243 {
244 SipMessage& request = mDialogSet.getCreator()->getLastRequest();
245 if (request.header(h_RequestLine).method() == INVITE)
246 {
247 makeCancel(request);
248 mDum.send(request);
249 delete this;
250 }
251 else
252 {
253 throw new UsageUseException("Can only CANCEL an INVITE", __FILE__, __LINE__);
254 }
255 }
256 else
257 {
258 throw new UsageUseException("Attempting to cancel UAS dialogSet", __FILE__, __LINE__);
259 }
260 }
261 }
262
263 void
264 Dialog::dispatch(const SipMessage& msg)
265 {
266 InfoLog ( << "Dialog::dispatch: " << msg.brief());
267 if (msg.isRequest())
268 {
269 const SipMessage& request = msg;
270 switch (request.header(h_CSeq).method())
271 {
272 case INVITE: // new INVITE
273 if (mInviteSession == 0)
274 {
275 InfoLog ( << "Dialog::dispatch -- Created new server invite session" << msg.brief());
276 mInviteSession = makeServerInviteSession(request);
277 }
278 mInviteSession->dispatch(request);
279 break;
280 case BYE:
281 if (mInviteSession == 0)
282 {
283 InfoLog ( << "Spurious BYE" );
284 return;
285 }
286 else
287 {
288 mInviteSession->dispatch(request);
289 }
290 break;
291 case ACK:
292 case CANCEL:
293 if (mInviteSession == 0)
294 {
295 InfoLog (<< "Drop stray ACK or CANCEL in dialog on the floor");
296 DebugLog (<< request);
297 }
298 else
299 {
300 mInviteSession->dispatch(request);
301 }
302 break;
303 case SUBSCRIBE:
304 {
305 ServerSubscription* server = findMatchingServerSub(request);
306 if (server)
307 {
308 server->dispatch(request);
309 }
310 else
311 {
312 if (request.header(h_Event).value() == "refer")
313 {
314 InfoLog (<< "Received a subscribe to a non-existent refer subscription: " << request.brief());
315 SipMessage failure;
316 makeResponse(failure, request, 403);
317 mDum.sendResponse(failure);
318 return;
319 }
320 else
321 {
322 server = makeServerSubscription(request);
323 mServerSubscriptions.push_back(server);
324 server->dispatch(request);
325 }
326 }
327 mDum.mInviteSessionHandler->onRefer(mInviteSession->getSessionHandle(), server->getHandle(), msg);
328 }
329 break;
330 case REFER:
331 {
332 if (mInviteSession == 0)
333 {
334 InfoLog (<< "Received an in dialog refer in a non-invite dialog: " << request.brief());
335 SipMessage failure;
336 makeResponse(failure, request, 405);
337 mDum.sendResponse(failure);
338 return;
339 }
340 else if (!request.exists(h_ReferTo))
341 {
342 InfoLog (<< "Received refer w/out a Refer-To: " << request.brief());
343 SipMessage failure;
344 makeResponse(failure, request, 400);
345 mDum.sendResponse(failure);
346 return;
347 }
348 else
349 {
350 ServerSubscription* server = findMatchingServerSub(request);
351 if (server)
352 {
353 server->dispatch(request);
354 }
355 else
356 {
357 server = makeServerSubscription(request);
358 mServerSubscriptions.push_back(server);
359 server->dispatch(request);
360 }
361 mDum.mInviteSessionHandler->onRefer(mInviteSession->getSessionHandle(), server->getHandle(), msg);
362 }
363 }
364 break;
365 case NOTIFY:
366 {
367 ClientSubscription* client = findMatchingClientSub(request);
368 if (client)
369 {
370 client->dispatch(request);
371 }
372 else
373 {
374 BaseCreator* creator = mDum.findCreator(mId);
375 if (creator)
376 {
377 ClientSubscription* sub = makeClientSubscription(request);
378 mClientSubscriptions.push_back(sub);
379 sub->dispatch(request);
380 }
381 else
382 {
383 SipMessage failure;
384 makeResponse(failure, request, 481);
385 mDum.sendResponse(failure);
386 return;
387 }
388 }
389 }
390 break;
391 default:
392 assert(0);
393 return;
394 }
395 }
396 else if (msg.isResponse())
397 {
398 if (!mDialogSet.getCreator() ||
399 !(msg.header(h_CSeq).method() == mDialogSet.getCreator()->getLastRequest().header(h_RequestLine).method()))
400 {
401 SipMessage* lastRequest = 0;
402 switch (msg.header(h_CSeq).method())
403 {
404 case INVITE:
405 case CANCEL:
406 case REFER:
407 if (mInviteSession == 0)
408 {
409 //spurious
410 return;
411 }
412 else
413 {
414 lastRequest = &mInviteSession->mLastRequest;
415 }
416 break;
417 default:
418 break;
419 }
420 if ( lastRequest && mDum.mClientAuthManager->handle( *lastRequest, msg ) )
421 {
422 InfoLog( << "about to retransmit request with digest credentials" );
423 InfoLog( << *lastRequest );
424
425 mDum.send(*lastRequest);
426 return;
427 }
428 }
429
430 const SipMessage& response = msg;
431 // !jf! should this only be for 2xx responses? !jf! Propose no as an
432 // answer !dcm! what is he on?
433 switch (response.header(h_CSeq).method())
434 {
435 case INVITE:
436 if (mInviteSession == 0)
437 {
438 // #if!jf! don't think creator needs a dispatch
439 //BaseCreator* creator = mDum.findCreator(mId);
440 //assert (creator); // stray responses have been rejected already
441 //creator->dispatch(response);
442 // #endif!jf!
443 InfoLog ( << "Dialog::dispatch -- Created new client invite session" << msg.brief());
444
445 mInviteSession = makeClientInviteSession(response);
446 mInviteSession->dispatch(response);
447 }
448 else
449 {
450 mInviteSession->dispatch(response);
451 }
452 break;
453 case BYE:
454 case ACK:
455 case CANCEL:
456 if (mInviteSession != 0)
457 {
458 mInviteSession->dispatch(response);
459 }
460 // else drop on the floor
461 break;
462 case REFER:
463 {
464 int code = response.header(h_StatusLine).statusCode();
465 if (code < 300)
466 {
467 // throw it away
468 return;
469 }
470 else
471 {
472 if (mInviteSession && mDum.mInviteSessionHandler)
473 {
474 mDum.mInviteSessionHandler->onReferRejected(mInviteSession->getSessionHandle(), response);
475 }
476 }
477 }
478 break;
479 case SUBSCRIBE:
480 {
481 int code = response.header(h_StatusLine).statusCode();
482 if (code < 300)
483 {
484 // throw it away
485 return;
486 }
487 else
488 {
489 ClientSubscription* client = findMatchingClientSub(response);
490 if (client)
491 {
492 client->dispatch(response);
493 }
494 else
495 {
496 //!dcm! -- can't subscribe in an existing Dialog, this is all
497 //a bit of a hack.
498 BaseCreator* creator = mDialogSet.getCreator();
499 assert(creator);
500 assert(creator->getLastRequest().exists(h_Event));
501 ClientSubscriptionHandler* handler =
502 mDum.getClientSubscriptionHandler(creator->getLastRequest().header(h_Event).value());
503 assert(handler);
504 handler->onTerminated(ClientSubscriptionHandle::NotValid(), response);
505 possiblyDie();
506 }
507 }
508 }
509 break;
510 case NOTIFY:
511 {
512 //only dispatch if there is a matching server subscription. DUM does
513 //not handle responses to out-of-dialog NOTIFY messages
514 ServerSubscription* server = findMatchingServerSub(response);
515 if (server)
516 {
517 server->dispatch(response);
518 }
519 }
520 default:
521 assert(0);
522 return;
523 }
524 }
525 }
526
527 ServerSubscription*
528 Dialog::findMatchingServerSub(const SipMessage& msg)
529 {
530 for (std::list<ServerSubscription*>::iterator i=mServerSubscriptions.begin();
531 i != mServerSubscriptions.end(); ++i)
532 {
533 if ((*i)->matches(msg))
534 {
535 return *i;
536 }
537 }
538 return 0;
539 }
540
541 ClientSubscription*
542 Dialog::findMatchingClientSub(const SipMessage& msg)
543 {
544 for (std::list<ClientSubscription*>::iterator i=mClientSubscriptions.begin();
545 i != mClientSubscriptions.end(); ++i)
546 {
547 if ((*i)->matches(msg))
548 {
549 return *i;
550 }
551 }
552 return 0;
553 }
554
555 InviteSessionHandle
556 Dialog::getInviteSession()
557 {
558 if (mInviteSession)
559 {
560 return mInviteSession->getSessionHandle();
561 }
562 else
563 {
564 return InviteSessionHandle::NotValid();
565 }
566 }
567
568 std::vector<ClientSubscriptionHandle>
569 Dialog::findClientSubscriptions(const Data& event)
570 {
571 std::vector<ClientSubscriptionHandle> handles;
572
573 for (std::list<ClientSubscription*>::const_iterator i = mClientSubscriptions.begin();
574 i != mClientSubscriptions.end(); ++i)
575 {
576 if ( (*i)->getEventType() == event)
577 {
578 handles.push_back((*i)->getHandle());
579 }
580 }
581 return handles;
582 }
583
584 std::vector<ServerSubscriptionHandle>
585 Dialog::findServerSubscriptions(const Data& event)
586 {
587 std::vector<ServerSubscriptionHandle> handles;
588
589 for (std::list<ServerSubscription*>::const_iterator i = mServerSubscriptions.begin();
590 i != mServerSubscriptions.end(); ++i)
591 {
592 if ( (*i)->getEventType() == event)
593 {
594 handles.push_back((*i)->getHandle());
595 }
596 }
597 return handles;
598 }
599
600
601 std::vector<ClientSubscriptionHandle>
602 Dialog::getClientSubscriptions()
603 {
604 std::vector<ClientSubscriptionHandle> handles;
605
606 for (std::list<ClientSubscription*>::const_iterator i = mClientSubscriptions.begin();
607 i != mClientSubscriptions.end(); ++i)
608 {
609 handles.push_back((*i)->getHandle());
610 }
611
612 return handles;
613 }
614
615 std::vector<ServerSubscriptionHandle>
616 Dialog::getServerSubscriptions()
617 {
618 std::vector<ServerSubscriptionHandle> handles;
619
620 for (std::list<ServerSubscription*>::const_iterator i = mServerSubscriptions.begin();
621 i != mServerSubscriptions.end(); ++i)
622 {
623 handles.push_back((*i)->getHandle());
624 }
625
626 return handles;
627 }
628
629
630 #if 0
631 void
632 Dialog::processNotify(const SipMessage& notify)
633 {
634 if (notify.isRequest())
635 {
636 if (findSubscriptions().empty())
637 {
638 SubscriptionCreator* creator = dynamic_cast<SubscriptionCreator*>(DialogSetId(notify).getCreator());
639 if (creator)
640 {
641 creator->makeNewSubscription(notify);
642 }
643 }
644 else
645 {
646 for (std::list<BaseUsage*>::iterator i=mUsages.begin(); i!=mUsages.end(); i++)
647 {
648 ClientSubscription* sub = dynamic_cast<ClientSubscription*>(*i);
649 if (sub && sub->matches(notify))
650 {
651 sub->process(notify);
652 break;
653 }
654 }
655 }
656 }
657 }
658 #endif
659
660
661 void
662 Dialog::makeRequest(SipMessage& request, MethodTypes method)
663 {
664 RequestLine rLine(method);
665
666 rLine.uri() = mRemoteTarget.uri();
667
668 request.header(h_RequestLine) = rLine;
669 request.header(h_To) = mRemoteNameAddr;
670 // request.header(h_To).param(p_tag) = mId.getRemoteTag();
671 request.header(h_From) = mLocalNameAddr;
672 // request.header(h_From).param(p_tag) = mId.getLocalTag();
673
674 request.header(h_CallId) = mCallId;
675
676 request.remove(h_RecordRoutes); //!dcm! -- all of this is rather messy
677
678 request.remove(h_Contacts);
679 request.header(h_Contacts).push_front(mLocalContact);
680 request.header(h_CSeq).method() = method;
681 request.header(h_MaxForwards).value() = 70;
682
683 //must keep old via for cancel
684 if (method != CANCEL)
685 {
686 request.header(h_Routes) = mRouteSet;
687 request.remove(h_Vias);
688 Via via;
689 via.param(p_branch); // will create the branch
690 request.header(h_Vias).push_front(via);
691 }
692 else
693 {
694 assert(request.exists(h_Vias));
695 }
696 //don't increment CSeq for ACK or CANCEL
697 if (method != ACK && method != CANCEL)
698 {
699 request.header(h_CSeq).sequence() = ++mLocalCSeq;
700 }
701 InfoLog ( << "Dialog::makeRequest: " << request );
702 }
703
704 void
705 Dialog::makeCancel(SipMessage& request)
706 {
707 makeRequest(request, CANCEL);
708
709 //not allowed in a CANCEL
710 request.remove(h_Requires);
711 request.remove(h_ProxyRequires);
712 request.header(h_To).remove(p_tag);
713 }
714
715 void
716 Dialog::makeResponse(SipMessage& response, const SipMessage& request, int code)
717 {
718 assert( code >= 100 );
719 if (code < 300 && code > 100)
720 {
721 assert(request.isRequest());
722 assert(request.header(h_RequestLine).getMethod() == INVITE ||
723 request.header(h_RequestLine).getMethod() == SUBSCRIBE ||
724 request.header(h_RequestLine).getMethod() == BYE ||
725 request.header(h_RequestLine).getMethod() == CANCEL ||
726 request.header(h_RequestLine).getMethod() == NOTIFY
727 );
728
729 assert (request.header(h_RequestLine).getMethod() == CANCEL || // Contact header is not required for Requests that do not form a dialog
730 request.header(h_RequestLine).getMethod() == BYE ||
731 request.header(h_Contacts).size() == 1);
732 Helper::makeResponse(response, request, code, mLocalContact);
733 response.header(h_To).param(p_tag) = mId.getLocalTag();
734 }
735 else
736 {
737 Helper::makeResponse(response, request, code, mLocalContact);
738 response.header(h_To).param(p_tag) = mId.getLocalTag();
739
740 }
741 InfoLog ( << "Dialog::makeResponse: " << response);
742 }
743
744
745 ClientInviteSession*
746 Dialog::makeClientInviteSession(const SipMessage& response)
747 {
748 InviteSessionCreator* creator = dynamic_cast<InviteSessionCreator*>(mDialogSet.getCreator());
749 assert(creator); // !jf! this maybe can assert by evil UAS
750 //return mDum.createAppClientInviteSession(*this, *creator);
751 return new ClientInviteSession(mDum, *this, creator->getLastRequest(),
752 creator->getInitialOffer(), creator->getServerSubscription());
753 }
754
755
756
757 ClientSubscription*
758 Dialog::makeClientSubscription(const SipMessage& request)
759 {
760 return new ClientSubscription(mDum, *this, request);
761 }
762
763
764 ServerInviteSession*
765 Dialog::makeServerInviteSession(const SipMessage& request)
766 {
767 return new ServerInviteSession(mDum, *this, request);
768 }
769
770 ServerSubscription*
771 Dialog::makeServerSubscription(const SipMessage& request)
772 {
773 return new ServerSubscription(mDum, *this, request);
774 }
775
776 Dialog::Exception::Exception(const Data& msg, const Data& file, int line)
777 : BaseException(msg, file, line)
778 {
779 }
780
781 void
782 Dialog::update(const SipMessage& msg)
783 {
784 }
785
786 #if 0
787 void
788 Dialog::setLocalContact(const NameAddr& localContact)
789 {
790 mLocalContact = localContact;
791 }
792
793 void
794 Dialog::setRemoteTarget(const NameAddr& remoteTarget)
795 {
796 mRemoteTarget = remoteTarget;
797 }
798 #endif
799
800 void Dialog::possiblyDie()
801 {
802 if (!mDestroying)
803 {
804 if (mClientSubscriptions.empty() &&
805 mServerSubscriptions.empty() &&
806 !mInviteSession)
807 {
808 delete this;
809 }
810 }
811 }
812
813 ostream&
814 resip::operator<<(ostream& strm, const Dialog& dialog)
815 {
816
817 return strm;
818 }
819

webmaster AT resiprocate DOT org
ViewVC Help
Powered by ViewVC 1.1.27