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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3241 - (show annotations) (download)
Tue Aug 10 21:29:00 2004 UTC (15 years, 4 months ago) by derek
Original Path: main/sip/resiprocate/dum/InviteSession.cxx
File size: 21879 byte(s)
reset the SDP state machine when a re-invite fails
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 else
240 {
241 //reset the sdp state machine
242 incomingSdp(msg, 0);
243 }
244 }
245 else
246 {
247 mDum.mInviteSessionHandler->onOfferRejected(getSessionHandle(), msg);
248 mState = Connected;
249 }
250 }
251 else
252 {
253 ErrLog ( << "Spurious message sent to UAS " << msg );
254 return;
255 }
256 break;
257 case Accepting:
258 if (msg.isRequest() && msg.header(h_RequestLine).method() == ACK)
259 {
260 mState = Connected;
261 mDum.mInviteSessionHandler->onConnected(getSessionHandle(), msg);
262 if (offans.first != None)
263 {
264 InviteSession::incomingSdp(msg, offans.second);
265 }
266 }
267 else
268 {
269 ErrLog ( << "Spurious message sent to UAS " << msg );
270 return;
271 }
272 break;
273 case AcceptingReInvite:
274 if (msg.isRequest() && msg.header(h_RequestLine).method() == ACK)
275 {
276 mState = Connected;
277 //this shouldn't happen, but it may be allowed(DUM API doesn't
278 //support this for re-invite)
279 if (offans.first != None)
280 {
281 InviteSession::incomingSdp(msg, offans.second);
282 }
283 }
284 else
285 {
286 ErrLog ( << "Spurious message sent to UAS " << msg );
287 return;
288 }
289 break;
290
291
292 default:
293 DebugLog ( << "Throwing away strange message: " << msg );
294 //throw message away
295 // assert(0); //all other cases should be handled in base classes
296
297 }
298 }
299
300 SipMessage&
301 InviteSession::makeRefer(const NameAddr& referTo)
302 {
303 mDialog.makeRequest(mLastRequest, REFER);
304 mLastRequest.header(h_ReferTo) = referTo;
305 return mLastRequest;
306 }
307
308 SipMessage&
309 InviteSession::makeRefer(const NameAddr& referTo, InviteSessionHandle sessionToReplace)
310 {
311 if (!sessionToReplace.isValid())
312 {
313 throw new UsageUseException("Attempted to make a refer w/ and invalid replacement target", __FILE__, __LINE__);
314 }
315
316 mDialog.makeRequest(mLastRequest, REFER);
317 mLastRequest.header(h_ReferTo) = referTo;
318 CallId replaces;
319 DialogId id = sessionToReplace->mDialog.getId();
320 replaces.value() = id.getCallId();
321 replaces.param(p_toTag) = id.getRemoteTag();
322 replaces.param(p_fromTag) = id.getLocalTag();
323
324 mLastRequest.header(h_ReferTo).uri().embedded().header(h_Replaces) = replaces;
325 return mLastRequest;
326 }
327
328 SipMessage&
329 InviteSession::end()
330 {
331 InfoLog ( << "InviteSession::end, state: " << mState);
332 switch (mState)
333 {
334 case Terminated:
335 throw UsageUseException("Cannot end a session that has already been cancelled.", __FILE__, __LINE__);
336 break;
337 case Connected:
338 case Accepting:
339 InfoLog ( << "InviteSession::end, connected or Accepting" );
340 mDialog.makeRequest(mLastRequest, BYE);
341 //new transaction
342 assert(mLastRequest.header(h_Vias).size() == 1);
343 // mLastRequest.header(h_Vias).front().param(p_branch).reset();
344 mState = Terminated;
345 return mLastRequest;
346 break;
347 default:
348 assert(0); // out of states
349 }
350 throw UsageUseException("Programmer error", __FILE__, __LINE__); //make VC++ happy
351 }
352
353 // If sdp==0, it means the last offer failed
354 // !dcm! -- eventually handle confused UA's that send offers/answers at
355 // inappropriate times, probably with a different callback
356 void
357 InviteSession::incomingSdp(const SipMessage& msg, const SdpContents* sdp)
358 {
359 switch (mOfferState)
360 {
361 case Nothing:
362 assert(mCurrentLocalSdp == 0);
363 assert(mCurrentRemoteSdp == 0);
364 assert(mProposedLocalSdp == 0);
365 assert(mProposedRemoteSdp == 0);
366 mProposedRemoteSdp = static_cast<SdpContents*>(sdp->clone());
367 mOfferState = Offerred;
368 mDum.mInviteSessionHandler->onOffer(getSessionHandle(), msg, sdp);
369 break;
370
371 case Offerred:
372 assert(mCurrentLocalSdp == 0);
373 assert(mCurrentRemoteSdp == 0);
374 mCurrentLocalSdp = mProposedLocalSdp;
375 mCurrentRemoteSdp = static_cast<SdpContents*>(sdp->clone());
376 delete mProposedRemoteSdp;
377 mProposedLocalSdp = 0;
378 mProposedRemoteSdp = 0;
379 mOfferState = Answered;
380 mDum.mInviteSessionHandler->onAnswer(getSessionHandle(), msg, sdp);
381 break;
382
383 case Answered:
384 assert(mProposedLocalSdp == 0);
385 assert(mProposedRemoteSdp == 0);
386 mProposedRemoteSdp = static_cast<SdpContents*>(sdp->clone());
387 mOfferState = CounterOfferred;
388 mDum.mInviteSessionHandler->onOffer(getSessionHandle(), msg, sdp);
389 break;
390
391 case CounterOfferred:
392 assert(mCurrentLocalSdp);
393 assert(mCurrentRemoteSdp);
394 mOfferState = Answered;
395 if (sdp) // !slg! There currenlty doesn't seem to be anyone calling this with sdp == 0
396 {
397 delete mCurrentLocalSdp;
398 delete mCurrentRemoteSdp;
399 mCurrentLocalSdp = mProposedLocalSdp;
400 mCurrentRemoteSdp = static_cast<SdpContents*>(sdp->clone());
401 delete mProposedRemoteSdp;
402 mProposedLocalSdp = 0;
403 mProposedRemoteSdp = 0;
404 mOfferState = Answered;
405 mDum.mInviteSessionHandler->onAnswer(getSessionHandle(), msg, sdp);
406 }
407 else
408 {
409 delete mProposedLocalSdp;
410 delete mProposedRemoteSdp;
411 mProposedLocalSdp = 0;
412 mProposedRemoteSdp = 0;
413 // !jf! is this right?
414 mDum.mInviteSessionHandler->onOfferRejected(getSessionHandle(), msg);
415 }
416 break;
417 }
418 }
419
420 void
421 InviteSession::send(SipMessage& msg)
422 {
423 Destroyer::Guard guard(mDestroyer);
424 if (msg.isRequest())
425 {
426 //unless the message is an ACK(in which case it is mAck)
427 //strip out the SDP after sending
428 switch(msg.header(h_RequestLine).getMethod())
429 {
430 case INVITE:
431 case UPDATE:
432 if (mNextOfferOrAnswerSdp)
433 {
434 msg.setContents(mNextOfferOrAnswerSdp);
435 sendSdp(mNextOfferOrAnswerSdp);
436 mNextOfferOrAnswerSdp = 0;
437 }
438 break;
439 default:
440 break;
441 }
442
443 if (msg.header(h_RequestLine).getMethod() == ACK)
444 {
445 mDum.send(msg);
446 }
447 else
448 {
449 mDum.send(msg);
450 msg.releaseContents();
451 }
452 }
453 else
454 {
455 int code = msg.header(h_StatusLine).statusCode();
456 //!dcm! -- probably kill this object earlier, handle 200 to bye in
457 //DialogUsageManager...very soon
458 if (msg.header(h_CSeq).method() == BYE && code == 200) //!dcm! -- not 2xx?
459
460 {
461 mState = Terminated;
462 mDum.send(msg);
463 //mDum.mInviteSessionHandler->onTerminated(getSessionHandle(), msg); // This is actually called when recieving the BYE message so that the BYE message can be passed to onTerminated
464 guard.destroy();
465 }
466 else if (code >= 200 && code < 300 && msg.header(h_CSeq).method() == INVITE)
467 {
468 assert(&msg == &mFinalResponse);
469 mCurrentRetransmit200 = T1;
470 mDum.addTimerMs(DumTimeout::Retransmit200, mCurrentRetransmit200, getBaseHandle(), 0);
471 mDum.addTimerMs(DumTimeout::WaitForAck, TimerH, getBaseHandle(), 0);
472
473 //!dcm! -- this should be mFinalResponse...maybe assign here in
474 //case the user wants to be very strange
475 if (mNextOfferOrAnswerSdp)
476 {
477 msg.setContents(mNextOfferOrAnswerSdp);
478 sendSdp(mNextOfferOrAnswerSdp);
479 mNextOfferOrAnswerSdp = 0;
480 }
481 mDum.send(msg);
482 }
483 else
484 {
485 mDum.send(msg);
486 msg.releaseContents();
487 }
488 }
489 }
490
491 void
492 InviteSession::sendSdp(SdpContents* sdp)
493 {
494 switch (mOfferState)
495 {
496 case Nothing:
497 assert(mCurrentLocalSdp == 0);
498 assert(mCurrentRemoteSdp == 0);
499 mProposedLocalSdp = sdp;
500 mOfferState = Offerred;
501 break;
502
503 case Offerred:
504 assert(mCurrentLocalSdp == 0);
505 assert(mCurrentRemoteSdp == 0);
506 mCurrentLocalSdp = sdp;
507 mCurrentRemoteSdp = mProposedRemoteSdp;
508 delete mProposedLocalSdp;
509 mProposedLocalSdp = 0;
510 mProposedRemoteSdp = 0;
511 mOfferState = Answered;
512 break;
513
514 case Answered:
515 assert(mProposedLocalSdp == 0);
516 assert(mProposedRemoteSdp == 0);
517 mProposedLocalSdp = sdp;
518 mOfferState = CounterOfferred;
519 break;
520
521 case CounterOfferred:
522 assert(mCurrentLocalSdp);
523 assert(mCurrentRemoteSdp);
524 if (sdp) // !slg! There currenlty doesn't seem to be anyone calling this with sdp == 0
525 {
526 delete mCurrentLocalSdp;
527 delete mCurrentRemoteSdp;
528 mCurrentLocalSdp = sdp;
529 mCurrentRemoteSdp = mProposedRemoteSdp;
530 delete mProposedLocalSdp;
531 mProposedLocalSdp = 0;
532 mProposedRemoteSdp = 0;
533 }
534 else
535 {
536 delete mProposedLocalSdp;
537 delete mProposedRemoteSdp;
538 mProposedLocalSdp = 0;
539 mProposedRemoteSdp = 0;
540 }
541 mOfferState = Answered;
542 break;
543 }
544 }
545
546 std::pair<InviteSession::OfferAnswerType, const SdpContents*>
547 InviteSession::getOfferOrAnswer(const SipMessage& msg) const
548 {
549 std::pair<InviteSession::OfferAnswerType, const SdpContents*> ret;
550 ret.first = None;
551
552 const SdpContents* contents = dynamic_cast<const SdpContents*>(msg.getContents());
553 if (contents)
554 {
555 static Token c100rel(Symbols::C100rel);
556 if (msg.isRequest() || msg.header(h_StatusLine).responseCode() == 200 ||
557 msg.exists(h_Supporteds) && msg.header(h_Supporteds).find(c100rel))
558 {
559 switch (mOfferState)
560 {
561 case None:
562 ret.first = Offer;
563 ret.second = contents;
564 break;
565
566 case Offerred:
567 ret.first = Answer;
568 ret.second = contents;
569 break;
570
571 case Answered:
572 ret.first = Offer;
573 ret.second = contents;
574 break;
575
576 case CounterOfferred:
577 ret.first = Answer;
578 ret.second = contents;
579 break;
580 }
581 }
582 }
583 return ret;
584 }
585
586 void
587 InviteSession::copyAuthorizations(SipMessage& request)
588 {
589 #if 0
590 if (mLastRequest.exists(h_ProxyAuthorizations))
591 {
592 // should make the next auth (change nextNonce)
593 request.header(h_ProxyAuthorizations) = mLastRequest.header(h_ProxyAuthorizations);
594 }
595 if (mLastRequest.exists(h_ProxyAuthorizations))
596 {
597 // should make the next auth (change nextNonce)
598 request.header(h_ProxyAuthorizations) = mLastRequest.header(h_ProxyAuthorizations);
599 }
600 #endif
601 }
602
603 SipMessage&
604 InviteSession::rejectOffer(int statusCode)
605 {
606 if (statusCode < 400)
607 {
608 throw new UsageUseException("Must reject with a 4xx", __FILE__, __LINE__);
609 }
610 //sdp state change here--go to initial state?
611 mDialog.makeResponse(mLastResponse, mLastRequest, statusCode);
612 return mLastResponse;
613 }
614
615 SipMessage&
616 InviteSession::targetRefresh(const NameAddr& localUri)
617 {
618 assert(0);
619 return mLastRequest;
620 }
621
622 SipMessage&
623 InviteSession::ackConnection()
624 {
625 //if not a reinvite, and a pending offer exists, throw
626 makeAck();
627 //new transaction
628 assert(mAck.header(h_Vias).size() == 1);
629 // mAck.header(h_Vias).front().param(p_branch).reset();
630 return mAck;
631 }
632
633 void
634 InviteSession::makeAck()
635 {
636 mAck = mLastRequest;
637
638 InfoLog ( << "InviteSession::makeAck:before: " << mAck );
639
640 mDialog.makeRequest(mAck, ACK);
641 if (mNextOfferOrAnswerSdp)
642 {
643 mAck.setContents(mNextOfferOrAnswerSdp);
644 sendSdp(mNextOfferOrAnswerSdp);
645 mNextOfferOrAnswerSdp = 0;
646 }
647
648 InfoLog ( << "InviteSession::makeAck:after: " << mAck );
649 }
650
651 /* ====================================================================
652 * The Vovida Software License, Version 1.0
653 *
654 * Copyright (c) 2000 Vovida Networks, Inc. All rights reserved.
655 *
656 * Redistribution and use in source and binary forms, with or without
657 * modification, are permitted provided that the following conditions
658 * are met:
659 *
660 * 1. Redistributions of source code must retain the above copyright
661 * notice, this list of conditions and the following disclaimer.
662 *
663 * 2. Redistributions in binary form must reproduce the above copyright
664 * notice, this list of conditions and the following disclaimer in
665 * the documentation and/or other materials provided with the
666
667 * distribution.
668 *
669 * 3. The names "VOCAL", "Vovida Open Communication Application Library",
670 * and "Vovida Open Communication Application Library (VOCAL)" must
671 * not be used to endorse or promote products derived from this
672 * software without prior written permission. For written
673 * permission, please contact vocal@vovida.org.
674 *
675 * 4. Products derived from this software may not be called "VOCAL", nor
676 * may "VOCAL" appear in their name, without prior written
677 * permission of Vovida Networks, Inc.
678 *
679 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
680 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
681 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND
682 * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA
683 * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES
684 * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
685 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
686 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
687 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
688 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
689 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
690 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
691 * DAMAGE.
692 *
693 * ====================================================================
694 *
695 * This software consists of voluntary contributions made by Vovida
696 * Networks, Inc. and many individuals on behalf of Vovida Networks,
697 * Inc. For more information on Vovida Networks, Inc., please see
698 * <http://www.vovida.org/>.
699 *
700 */

webmaster AT resiprocate DOT org
ViewVC Help
Powered by ViewVC 1.1.27