/[resiprocate]/branches/b-directory-reorg/resip/dum/InviteSession.cxx
ViewVC logotype

Contents of /branches/b-directory-reorg/resip/dum/InviteSession.cxx

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3167 - (show annotations) (download)
Wed Jul 28 19:28:32 2004 UTC (15 years, 5 months ago) by derek
Original Path: main/sip/resiprocate/dum/InviteSession.cxx
File size: 21703 byte(s)
re-invite rejection callback, loose to tag matching(vonage) default in profile now false.
1 #include "resiprocate/SdpContents.hxx"
2 #include "resiprocate/SipMessage.hxx"
3 #include "resiprocate/dum/Dialog.hxx"
4 #include "resiprocate/dum/DialogUsageManager.hxx"
5 #include "resiprocate/dum/InviteSession.hxx"
6 #include "resiprocate/dum/InviteSessionHandler.hxx"
7 #include "resiprocate/dum/Profile.hxx"
8 #include "resiprocate/dum/UsageUseException.hxx"
9 #include "resiprocate/os/Logger.hxx"
10
11 #if defined(WIN32) && defined(_DEBUG) &&defined(LEAK_CHECK)// Used for tracking down memory leaks in Visual Studio
12 #define _CRTDBG_MAP_ALLOC
13 #include <stdlib.h>
14 #include <crtdbg.h>
15 #define new new( _NORMAL_BLOCK, __FILE__, __LINE__)
16 #endif // defined(WIN32) && defined(_DEBUG)
17
18
19 #define RESIPROCATE_SUBSYSTEM Subsystem::DUM
20
21 using namespace resip;
22
23 unsigned long
24 InviteSession::T1 = 500;
25
26 unsigned long
27 InviteSession::T2 = 8 * T1;
28
29 unsigned long
30 InviteSession::TimerH = 64 * T1;
31
32 InviteSession::InviteSession(DialogUsageManager& dum, Dialog& dialog, State initialState)
33 : DialogUsage(dum, dialog),
34 mState(initialState),
35 mOfferState(Nothing),
36 mCurrentLocalSdp(0),
37 mCurrentRemoteSdp(0),
38 mProposedLocalSdp(0),
39 mProposedRemoteSdp(0),
40 mNextOfferOrAnswerSdp(0),
41 mDestroyer(this),
42 mCurrentRetransmit200(0)
43 {
44 DebugLog ( << "^^^ InviteSession::InviteSession " << this);
45 assert(mDum.mInviteSessionHandler);
46 }
47
48 InviteSession::~InviteSession()
49 {
50 DebugLog ( << "^^^ InviteSession::~InviteSession " << this);
51 delete mCurrentLocalSdp;
52 delete mCurrentRemoteSdp;
53 delete mProposedLocalSdp;
54 delete mProposedRemoteSdp;
55 delete mNextOfferOrAnswerSdp;
56 mDialog.mInviteSession = 0;
57 }
58
59 SipMessage&
60 InviteSession::modifySession()
61 {
62 if (mNextOfferOrAnswerSdp == 0 || mState != Connected)
63 {
64 throw new UsageUseException("Must be in the connected state and have propsed an offer to call modifySession",
65 __FILE__, __LINE__);
66 }
67 mState = ReInviting;
68 mDialog.makeRequest(mLastRequest, INVITE);
69 return mLastRequest;
70 }
71
72
73 SipMessage&
74 InviteSession::acceptOffer(int statusCode)
75 {
76 if (mNextOfferOrAnswerSdp == 0 || mState != ReInviting)
77 {
78 throw new UsageUseException("Must be in the ReInviting state and have propsed an answer to call answerModifySession",
79 __FILE__, __LINE__);
80 }
81 mState = AcceptingReInvite;
82 mDialog.makeResponse(mFinalResponse, mLastRequest, statusCode);
83 return mFinalResponse;
84 }
85
86 void
87 InviteSession::setOffer(const SdpContents* sdp)
88 {
89 if (mProposedRemoteSdp)
90 {
91 throw UsageUseException("Cannot set an offer with an oustanding remote offer", __FILE__, __LINE__);
92 }
93 assert(mNextOfferOrAnswerSdp == 0);
94 mNextOfferOrAnswerSdp = static_cast<SdpContents*>(sdp->clone());
95 }
96
97 void
98 InviteSession::setAnswer(const SdpContents* sdp)
99 {
100 if (mProposedLocalSdp )
101 {
102 throw UsageUseException("Cannot set an answer with an oustanding offer", __FILE__, __LINE__);
103 }
104 assert(mNextOfferOrAnswerSdp == 0);
105 mNextOfferOrAnswerSdp = static_cast<SdpContents*>(sdp->clone());
106 }
107
108 const SdpContents*
109 InviteSession::getLocalSdp()
110 {
111 return mCurrentLocalSdp;
112 }
113
114 const SdpContents*
115 InviteSession::getRemoteSdp()
116 {
117 return mCurrentRemoteSdp;
118 }
119
120 InviteSessionHandle
121 InviteSession::getSessionHandle()
122 {
123 return InviteSessionHandle(mDum, getBaseHandle().getId());
124 }
125
126
127 void
128 InviteSession::dispatch(const DumTimeout& timeout)
129 {
130 Destroyer::Guard guard(mDestroyer);
131 if (timeout.type() == DumTimeout::Retransmit200 && (mState == Accepting || mState == AcceptingReInvite ))
132 {
133 mDum.send(mFinalResponse);
134 mDum.addTimerMs(DumTimeout::Retransmit200, resipMin(T2, mCurrentRetransmit200*2), getBaseHandle(), 0);
135 }
136 else if (timeout.type() == DumTimeout::WaitForAck && mState != Connected)
137 {
138 mDialog.makeResponse(mLastResponse, mLastRequest, 408);
139 mDum.mInviteSessionHandler->onTerminated(getSessionHandle(), mLastResponse);
140 guard.destroy();
141 }
142 }
143
144 void
145 InviteSession::dispatch(const SipMessage& msg)
146 {
147 Destroyer::Guard guard(mDestroyer);
148 std::pair<OfferAnswerType, const SdpContents*> offans;
149 offans = InviteSession::getOfferOrAnswer(msg);
150
151 switch(mState)
152 {
153 case Terminated:
154 //!dcm! -- 481 behaviour here, should pretty much die on anything
155 //eventually 200 to BYE could be handled further out
156 if (msg.isResponse())
157 {
158 int code = msg.header(h_StatusLine).statusCode();
159 if ((code == 200 && msg.header(h_CSeq).method() == BYE) || code > 399)
160 {
161 mDum.mInviteSessionHandler->onTerminated(getSessionHandle(), msg);
162 guard.destroy();
163 return;
164 }
165 }
166 else
167 {
168 //make a function to do this & the occurences of this in DialogUsageManager
169 SipMessage failure;
170 mDum.makeResponse(failure, msg, 481);
171 failure.header(h_AcceptLanguages) = mDum.mProfile->getSupportedLanguages();
172 mDum.sendResponse(failure);
173 }
174 break;
175 case Connected:
176 if (msg.isRequest())
177 {
178 switch(msg.header(h_RequestLine).method())
179 {
180 // reINVITE
181 case INVITE:
182 mState = ReInviting;
183 mDialog.update(msg);
184 mLastRequest = msg; // !slg!
185 mDum.mInviteSessionHandler->onDialogModified(getSessionHandle(), msg);
186 if (offans.first != None)
187 {
188 incomingSdp(msg, offans.second);
189 }
190 break;
191
192 case BYE:
193 mState = Terminated;
194 mDum.mInviteSessionHandler->onTerminated(getSessionHandle(), msg);
195 mDialog.makeResponse(mLastResponse, msg, 200);
196 send(mLastResponse);
197 break;
198
199 case UPDATE:
200 assert(0);
201 break;
202
203 case INFO:
204 mDum.mInviteSessionHandler->onInfo(getSessionHandle(), msg);
205 break;
206
207 case REFER:
208 //handled in Dialog
209 assert(0);
210 break;
211
212 default:
213 InfoLog (<< "Ignoring request in an INVITE dialog: " << msg.brief());
214 break;
215 }
216 }
217 else
218 {
219 //!dcm! -- need to change this logic for when we don't have an ACK yet
220 if ( msg.header(h_StatusLine).statusCode() == 200)
221 {
222 //retransmist ack
223 mDum.send(mAck);
224 }
225 }
226 break;
227 case ReInviting:
228 if (msg.isResponse() && msg.header(h_CSeq).method() == INVITE)
229 {
230 int code = msg.header(h_StatusLine).statusCode();
231 if (code >=200 && code < 300)
232 {
233 mState = Connected;
234 send(ackConnection());
235 if (offans.first != None)
236 {
237 incomingSdp(msg, offans.second);
238 }
239 }
240 else
241 {
242 mDum.mInviteSessionHandler->onOfferRejected(getSessionHandle(), msg);
243 }
244 }
245 else
246 {
247 ErrLog ( << "Spurious message sent to UAS " << msg );
248 return;
249 }
250 break;
251 case Accepting:
252 if (msg.isRequest() && msg.header(h_RequestLine).method() == ACK)
253 {
254 mState = Connected;
255 mDum.mInviteSessionHandler->onConnected(getSessionHandle(), msg);
256 if (offans.first != None)
257 {
258 InviteSession::incomingSdp(msg, offans.second);
259 }
260 }
261 else
262 {
263 ErrLog ( << "Spurious message sent to UAS " << msg );
264 return;
265 }
266 break;
267 case AcceptingReInvite:
268 if (msg.isRequest() && msg.header(h_RequestLine).method() == ACK)
269 {
270 mState = Connected;
271 //this shouldn't happen, but it may be allowed(DUM API doesn't
272 //support this for re-invite)
273 if (offans.first != None)
274 {
275 InviteSession::incomingSdp(msg, offans.second);
276 }
277 }
278 else
279 {
280 ErrLog ( << "Spurious message sent to UAS " << msg );
281 return;
282 }
283 break;
284
285
286 default:
287 DebugLog ( << "Throwing away strange message: " << msg );
288 //throw message away
289 // assert(0); //all other cases should be handled in base classes
290
291 }
292 }
293
294 SipMessage&
295 InviteSession::makeRefer(const NameAddr& referTo)
296 {
297 mDialog.makeRequest(mLastRequest, REFER);
298 mLastRequest.header(h_ReferTo) = referTo;
299 return mLastRequest;
300 }
301
302 SipMessage&
303 InviteSession::makeRefer(const NameAddr& referTo, InviteSessionHandle sessionToReplace)
304 {
305 if (!sessionToReplace.isValid())
306 {
307 throw new UsageUseException("Attempted to make a refer w/ and invalid replacement target", __FILE__, __LINE__);
308 }
309
310 mDialog.makeRequest(mLastRequest, REFER);
311 mLastRequest.header(h_ReferTo) = referTo;
312 CallId replaces;
313 DialogId id = sessionToReplace->mDialog.getId();
314 replaces.value() = id.getCallId();
315 replaces.param(p_toTag) = id.getRemoteTag();
316 replaces.param(p_fromTag) = id.getLocalTag();
317
318 mLastRequest.header(h_ReferTo).uri().embedded().header(h_Replaces) = replaces;
319 return mLastRequest;
320 }
321
322 SipMessage&
323 InviteSession::end()
324 {
325 InfoLog ( << "InviteSession::end, state: " << mState);
326 switch (mState)
327 {
328 case Terminated:
329 throw UsageUseException("Cannot end a session that has already been cancelled.", __FILE__, __LINE__);
330 break;
331 case Connected:
332 case Accepting:
333 InfoLog ( << "InviteSession::end, connected or Accepting" );
334 mDialog.makeRequest(mLastRequest, BYE);
335 //new transaction
336 assert(mLastRequest.header(h_Vias).size() == 1);
337 // mLastRequest.header(h_Vias).front().param(p_branch).reset();
338 mState = Terminated;
339 return mLastRequest;
340 break;
341 default:
342 assert(0); // out of states
343 }
344 throw UsageUseException("Programmer error", __FILE__, __LINE__); //make VC++ happy
345 }
346
347 // If sdp==0, it means the last offer failed
348 // !dcm! -- eventually handle confused UA's that send offers/answers at
349 // inappropriate times, probably with a different callback
350 void
351 InviteSession::incomingSdp(const SipMessage& msg, const SdpContents* sdp)
352 {
353 switch (mOfferState)
354 {
355 case Nothing:
356 assert(mCurrentLocalSdp == 0);
357 assert(mCurrentRemoteSdp == 0);
358 assert(mProposedLocalSdp == 0);
359 assert(mProposedRemoteSdp == 0);
360 mProposedRemoteSdp = static_cast<SdpContents*>(sdp->clone());
361 mOfferState = Offerred;
362 mDum.mInviteSessionHandler->onOffer(getSessionHandle(), msg, sdp);
363 break;
364
365 case Offerred:
366 assert(mCurrentLocalSdp == 0);
367 assert(mCurrentRemoteSdp == 0);
368 mCurrentLocalSdp = mProposedLocalSdp;
369 mCurrentRemoteSdp = static_cast<SdpContents*>(sdp->clone());
370 delete mProposedRemoteSdp;
371 mProposedLocalSdp = 0;
372 mProposedRemoteSdp = 0;
373 mOfferState = Answered;
374 mDum.mInviteSessionHandler->onAnswer(getSessionHandle(), msg, sdp);
375 break;
376
377 case Answered:
378 assert(mProposedLocalSdp == 0);
379 assert(mProposedRemoteSdp == 0);
380 mProposedRemoteSdp = static_cast<SdpContents*>(sdp->clone());
381 mOfferState = CounterOfferred;
382 mDum.mInviteSessionHandler->onOffer(getSessionHandle(), msg, sdp);
383 break;
384
385 case CounterOfferred:
386 assert(mCurrentLocalSdp);
387 assert(mCurrentRemoteSdp);
388 mOfferState = Answered;
389 if (sdp) // !slg! There currenlty doesn't seem to be anyone calling this with sdp == 0
390 {
391 delete mCurrentLocalSdp;
392 delete mCurrentRemoteSdp;
393 mCurrentLocalSdp = mProposedLocalSdp;
394 mCurrentRemoteSdp = static_cast<SdpContents*>(sdp->clone());
395 delete mProposedRemoteSdp;
396 mProposedLocalSdp = 0;
397 mProposedRemoteSdp = 0;
398 mOfferState = Answered;
399 mDum.mInviteSessionHandler->onAnswer(getSessionHandle(), msg, sdp);
400 }
401 else
402 {
403 delete mProposedLocalSdp;
404 delete mProposedRemoteSdp;
405 mProposedLocalSdp = 0;
406 mProposedRemoteSdp = 0;
407 // !jf! is this right?
408 mDum.mInviteSessionHandler->onOfferRejected(getSessionHandle(), msg);
409 }
410 break;
411 }
412 }
413
414 void
415 InviteSession::send(SipMessage& msg)
416 {
417 Destroyer::Guard guard(mDestroyer);
418 if (msg.isRequest())
419 {
420 //unless the message is an ACK(in which case it is mAck)
421 //strip out the SDP after sending
422 switch(msg.header(h_RequestLine).getMethod())
423 {
424 case INVITE:
425 case UPDATE:
426 if (mNextOfferOrAnswerSdp)
427 {
428 msg.setContents(mNextOfferOrAnswerSdp);
429 sendSdp(mNextOfferOrAnswerSdp);
430 mNextOfferOrAnswerSdp = 0;
431 }
432 break;
433 default:
434 break;
435 }
436
437 if (msg.header(h_RequestLine).getMethod() == ACK)
438 {
439 mDum.send(msg);
440 }
441 else
442 {
443 mDum.send(msg);
444 msg.releaseContents();
445 }
446 }
447 else
448 {
449 int code = msg.header(h_StatusLine).statusCode();
450 //!dcm! -- probably kill this object earlier, handle 200 to bye in
451 //DialogUsageManager...very soon
452 if (msg.header(h_CSeq).method() == BYE && code == 200) //!dcm! -- not 2xx?
453
454 {
455 mState = Terminated;
456 mDum.send(msg);
457 //mDum.mInviteSessionHandler->onTerminated(getSessionHandle(), msg); // This is actually called when recieving the BYE message so that the BYE message can be passed to onTerminated
458 guard.destroy();
459 }
460 else if (code >= 200 && code < 300 && msg.header(h_CSeq).method() == INVITE)
461 {
462 assert(&msg == &mFinalResponse);
463 mCurrentRetransmit200 = T1;
464 mDum.addTimerMs(DumTimeout::Retransmit200, mCurrentRetransmit200, getBaseHandle(), 0);
465 mDum.addTimerMs(DumTimeout::WaitForAck, TimerH, getBaseHandle(), 0);
466
467 //!dcm! -- this should be mFinalResponse...maybe assign here in
468 //case the user wants to be very strange
469 if (mNextOfferOrAnswerSdp)
470 {
471 msg.setContents(mNextOfferOrAnswerSdp);
472 sendSdp(mNextOfferOrAnswerSdp);
473 mNextOfferOrAnswerSdp = 0;
474 }
475 mDum.send(msg);
476 }
477 else
478 {
479 mDum.send(msg);
480 msg.releaseContents();
481 }
482 }
483 }
484
485 void
486 InviteSession::sendSdp(SdpContents* sdp)
487 {
488 switch (mOfferState)
489 {
490 case Nothing:
491 assert(mCurrentLocalSdp == 0);
492 assert(mCurrentRemoteSdp == 0);
493 mProposedLocalSdp = sdp;
494 mOfferState = Offerred;
495 break;
496
497 case Offerred:
498 assert(mCurrentLocalSdp == 0);
499 assert(mCurrentRemoteSdp == 0);
500 mCurrentLocalSdp = sdp;
501 mCurrentRemoteSdp = mProposedRemoteSdp;
502 delete mProposedLocalSdp;
503 mProposedLocalSdp = 0;
504 mProposedRemoteSdp = 0;
505 mOfferState = Answered;
506 break;
507
508 case Answered:
509 assert(mProposedLocalSdp == 0);
510 assert(mProposedRemoteSdp == 0);
511 mProposedLocalSdp = sdp;
512 mOfferState = CounterOfferred;
513 break;
514
515 case CounterOfferred:
516 assert(mCurrentLocalSdp);
517 assert(mCurrentRemoteSdp);
518 if (sdp) // !slg! There currenlty doesn't seem to be anyone calling this with sdp == 0
519 {
520 delete mCurrentLocalSdp;
521 delete mCurrentRemoteSdp;
522 mCurrentLocalSdp = sdp;
523 mCurrentRemoteSdp = mProposedRemoteSdp;
524 delete mProposedLocalSdp;
525 mProposedLocalSdp = 0;
526 mProposedRemoteSdp = 0;
527 }
528 else
529 {
530 delete mProposedLocalSdp;
531 delete mProposedRemoteSdp;
532 mProposedLocalSdp = 0;
533 mProposedRemoteSdp = 0;
534 }
535 mOfferState = Answered;
536 break;
537 }
538 }
539
540 std::pair<InviteSession::OfferAnswerType, const SdpContents*>
541 InviteSession::getOfferOrAnswer(const SipMessage& msg) const
542 {
543 std::pair<InviteSession::OfferAnswerType, const SdpContents*> ret;
544 ret.first = None;
545
546 const SdpContents* contents = dynamic_cast<const SdpContents*>(msg.getContents());
547 if (contents)
548 {
549 static Token c100rel(Symbols::C100rel);
550 if (msg.isRequest() || msg.header(h_StatusLine).responseCode() == 200 ||
551 msg.exists(h_Supporteds) && msg.header(h_Supporteds).find(c100rel))
552 {
553 switch (mOfferState)
554 {
555 case None:
556 ret.first = Offer;
557 ret.second = contents;
558 break;
559
560 case Offerred:
561 ret.first = Answer;
562 ret.second = contents;
563 break;
564
565 case Answered:
566 ret.first = Offer;
567 ret.second = contents;
568 break;
569
570 case CounterOfferred:
571 ret.first = Answer;
572 ret.second = contents;
573 break;
574 }
575 }
576 }
577 return ret;
578 }
579
580 void
581 InviteSession::copyAuthorizations(SipMessage& request)
582 {
583 #if 0
584 if (mLastRequest.exists(h_ProxyAuthorizations))
585 {
586 // should make the next auth (change nextNonce)
587 request.header(h_ProxyAuthorizations) = mLastRequest.header(h_ProxyAuthorizations);
588 }
589 if (mLastRequest.exists(h_ProxyAuthorizations))
590 {
591 // should make the next auth (change nextNonce)
592 request.header(h_ProxyAuthorizations) = mLastRequest.header(h_ProxyAuthorizations);
593 }
594 #endif
595 }
596
597 SipMessage&
598 InviteSession::rejectOffer(int statusCode)
599 {
600 if (statusCode < 400)
601 {
602 throw new UsageUseException("Must reject with a 4xx", __FILE__, __LINE__);
603 }
604 //sdp state change here--go to initial state?
605 mDialog.makeResponse(mLastResponse, mLastRequest, statusCode);
606 return mLastResponse;
607 }
608
609 SipMessage&
610 InviteSession::targetRefresh(const NameAddr& localUri)
611 {
612 assert(0);
613 return mLastRequest;
614 }
615
616 SipMessage&
617 InviteSession::ackConnection()
618 {
619 //if not a reinvite, and a pending offer exists, throw
620 makeAck();
621 //new transaction
622 assert(mAck.header(h_Vias).size() == 1);
623 // mAck.header(h_Vias).front().param(p_branch).reset();
624 return mAck;
625 }
626
627 void
628 InviteSession::makeAck()
629 {
630 mAck = mLastRequest;
631
632 InfoLog ( << "InviteSession::makeAck:before: " << mAck );
633
634 mDialog.makeRequest(mAck, ACK);
635 if (mNextOfferOrAnswerSdp)
636 {
637 mAck.setContents(mNextOfferOrAnswerSdp);
638 sendSdp(mNextOfferOrAnswerSdp);
639 mNextOfferOrAnswerSdp = 0;
640 }
641
642 InfoLog ( << "InviteSession::makeAck:after: " << mAck );
643 }
644
645 /* ====================================================================
646 * The Vovida Software License, Version 1.0
647 *
648 * Copyright (c) 2000 Vovida Networks, Inc. All rights reserved.
649 *
650 * Redistribution and use in source and binary forms, with or without
651 * modification, are permitted provided that the following conditions
652 * are met:
653 *
654 * 1. Redistributions of source code must retain the above copyright
655 * notice, this list of conditions and the following disclaimer.
656 *
657 * 2. Redistributions in binary form must reproduce the above copyright
658 * notice, this list of conditions and the following disclaimer in
659 * the documentation and/or other materials provided with the
660
661 * distribution.
662 *
663 * 3. The names "VOCAL", "Vovida Open Communication Application Library",
664 * and "Vovida Open Communication Application Library (VOCAL)" must
665 * not be used to endorse or promote products derived from this
666 * software without prior written permission. For written
667 * permission, please contact vocal@vovida.org.
668 *
669 * 4. Products derived from this software may not be called "VOCAL", nor
670 * may "VOCAL" appear in their name, without prior written
671 * permission of Vovida Networks, Inc.
672 *
673 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
674 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
675 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND
676 * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA
677 * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES
678 * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
679 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
680 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
681 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
682 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
683 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
684 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
685 * DAMAGE.
686 *
687 * ====================================================================
688 *
689 * This software consists of voluntary contributions made by Vovida
690 * Networks, Inc. and many individuals on behalf of Vovida Networks,
691 * Inc. For more information on Vovida Networks, Inc., please see
692 * <http://www.vovida.org/>.
693 *
694 */

webmaster AT resiprocate DOT org
ViewVC Help
Powered by ViewVC 1.1.27