/[resiprocate]/main/resip/recon/MOHParkServer/Server.cxx
ViewVC logotype

Contents of /main/resip/recon/MOHParkServer/Server.cxx

Parent Directory Parent Directory | Revision Log Revision Log


Revision 9493 - (show annotations) (download)
Sat Apr 7 10:56:50 2012 UTC (7 years, 9 months ago) by dpocock
File MIME type: text/plain
File size: 20238 byte(s)
Include config.h from even more places where it may be needed
1 #ifdef HAVE_CONFIG_H
2 #include "config.h"
3 #endif
4
5 #ifdef WIN32
6 #include <process.h>
7 #else
8 #include <spawn.h>
9 #endif
10
11 #include "Server.hxx"
12 #include "../UserAgent.hxx"
13 #include "AppSubsystem.hxx"
14 #include "WebAdmin.hxx"
15 #include "WebAdminThread.hxx"
16
17 #include <resip/stack/Tuple.hxx>
18 #include <rutil/DnsUtil.hxx>
19 #include <rutil/Log.hxx>
20 #include <rutil/Logger.hxx>
21 #include <rutil/ThreadIf.hxx>
22 #include <rutil/WinLeakCheck.hxx>
23
24 // sipX includes
25 #include <os/OsSysLog.h>
26
27 using namespace recon;
28 using namespace resip;
29 using namespace std;
30
31 #define RESIPROCATE_SUBSYSTEM AppSubsystem::MOHPARKSERVER
32
33 namespace mohparkserver
34 {
35
36 class SdpMessageDecorator : public MessageDecorator
37 {
38 public:
39 virtual ~SdpMessageDecorator() {}
40 virtual void decorateMessage(SipMessage &msg,
41 const Tuple &source,
42 const Tuple &destination,
43 const Data& sigcompId)
44 {
45 SdpContents* sdp = dynamic_cast<SdpContents*>(msg.getContents());
46 if(sdp && sdp->session().media().size() > 0 && sdp->session().connection().getAddress() == "0.0.0.0")
47 {
48 // Fill in IP and Port from source
49 sdp->session().connection().setAddress(Tuple::inet_ntop(source), source.ipVersion() == V6 ? SdpContents::IP6 : SdpContents::IP4);
50 sdp->session().origin().setAddress(Tuple::inet_ntop(source), source.ipVersion() == V6 ? SdpContents::IP6 : SdpContents::IP4);
51 DebugLog( << "SdpMessageDecorator: src=" << source << ", dest=" << destination << ", msg=" << endl << msg.brief());
52 }
53 }
54 virtual void rollbackMessage(SipMessage& msg) {} // Nothing to do
55 virtual MessageDecorator* clone() const { return new SdpMessageDecorator; }
56 };
57
58 class MyUserAgent : public UserAgent
59 {
60 public:
61 MyUserAgent(Server& server, SharedPtr<UserAgentMasterProfile> profile, resip::AfterSocketCreationFuncPtr socketFunc) :
62 UserAgent(&server, profile, socketFunc),
63 mServer(server) {}
64
65 virtual void onApplicationTimer(unsigned int id, unsigned int durationMs, unsigned int seq)
66 {
67 if(id == MAXPARKTIMEOUT)
68 {
69 mServer.onMaxParkTimeout((ParticipantHandle)seq);
70 }
71 else
72 {
73 InfoLog(<< "onApplicationTimeout: id=" << id << " dur=" << durationMs << " seq=" << seq);
74 }
75
76 }
77
78 virtual void onSubscriptionTerminated(SubscriptionHandle handle, unsigned int statusCode)
79 {
80 InfoLog(<< "onSubscriptionTerminated: handle=" << handle << " statusCode=" << statusCode);
81 }
82
83 virtual void onSubscriptionNotify(SubscriptionHandle handle, Data& notifyData)
84 {
85 InfoLog(<< "onSubscriptionNotify: handle=" << handle << " data=" << endl << notifyData);
86 }
87 private:
88 Server& mServer;
89 };
90
91 class MOHParkServerLogger : public ExternalLogger
92 {
93 public:
94 virtual ~MOHParkServerLogger() {}
95 /** return true to also do default logging, false to supress default logging. */
96 virtual bool operator()(Log::Level level,
97 const Subsystem& subsystem,
98 const Data& appName,
99 const char* file,
100 int line,
101 const Data& message,
102 const Data& messageWithHeaders)
103 {
104 // Log any warnings/errors to the screen and all MOHParkServer logging messages
105 if(level <= Log::Warning || subsystem.getSubsystem() == AppSubsystem::MOHPARKSERVER.getSubsystem())
106 {
107 resipCout << messageWithHeaders << endl;
108 }
109 return true;
110 }
111 };
112 MOHParkServerLogger g_MOHParkServerLogger;
113
114 Server::Server(ConfigParser& config) :
115 ConversationManager(false /* local audio? */, ConversationManager::sipXConversationMediaInterfaceMode),
116 mConfig(config),
117 mIsV6Avail(false),
118 mMyUserAgent(0),
119 mMOHManager(*this),
120 mParkManager(*this),
121 mWebAdmin(0),
122 mWebAdminThread(0)
123 {
124 // Initialize loggers
125 initializeResipLogging(mConfig.mLogFileMaxBytes, mConfig.mLogLevel, mConfig.mLogFilename);
126 if(!mConfig.mSipXLogFilename.empty())
127 {
128 //enableConsoleOutput(TRUE); // Allow sipX console output
129 OsSysLog::initialize(0, "MOHParkServer");
130 OsSysLog::setOutputFile(0, mConfig.mSipXLogFilename.c_str()) ;
131 }
132
133 InfoLog( << "MOHParkServer settings:");
134 InfoLog( << " MOH URI = " << mConfig.mMOHUri);
135 InfoLog( << " MOH Registration Time = " << mConfig.mMOHRegistrationTime);
136 InfoLog( << " MOH Filename URL = " << mConfig.mMOHFilenameUrl);
137 InfoLog( << " Park URI = " << mConfig.mParkUri);
138 InfoLog( << " Park Registration Time = " << mConfig.mParkRegistrationTime);
139 InfoLog( << " Park MOH Filename URL = " << mConfig.mParkMOHFilenameUrl);
140 InfoLog( << " Park Orbit Range Start = " << mConfig.mParkOrbitRangeStart);
141 InfoLog( << " Park Number of Orbits = " << mConfig.mParkNumOrbits);
142 InfoLog( << " Park Orbit Registration Time = " << mConfig.mParkOrbitRegistrationTime);
143 InfoLog( << " Local IP Address = " << mConfig.mAddress);
144 InfoLog( << " Override DNS Servers = " << mConfig.mDnsServers);
145 InfoLog( << " UDP Port = " << mConfig.mUdpPort);
146 InfoLog( << " TCP Port = " << mConfig.mTcpPort);
147 InfoLog( << " TLS Port = " << mConfig.mTlsPort);
148 InfoLog( << " TLS Domain = " << mConfig.mTlsDomain);
149 InfoLog( << " Keepalives = " << (mConfig.mKeepAlives ? "enabled" : "disabled"));
150 InfoLog( << " Outbound Proxy = " << mConfig.mOutboundProxy);
151 InfoLog( << " Media Port Range Start = " << mConfig.mMediaPortRangeStart);
152 InfoLog( << " Media Port Range Size = " << mConfig.mMediaPortRangeSize);
153 InfoLog( << " Log Level = " << mConfig.mLogLevel);
154
155 resip::Data ;
156
157 if(!mConfig.mAddress.empty())
158 {
159 // If address is specified in config file, then just use this address only
160 Tuple myTuple(mConfig.mAddress, mConfig.mUdpPort, UDP);
161 if(myTuple.ipVersion() == V6)
162 {
163 mIsV6Avail = true;
164 }
165 }
166 else
167 {
168 // If address in config is empty - query network interface info
169 std::list<std::pair<Data,Data> > interfaces = DnsUtil::getInterfaces();
170 std::list<std::pair<Data,Data> >::iterator itInt = interfaces.begin();
171 for(;itInt != interfaces.end(); itInt++)
172 {
173 Tuple myTuple(itInt->second, mConfig.mUdpPort, UDP);
174 if(myTuple.ipVersion() == V6)
175 {
176 mIsV6Avail = true;
177 }
178 }
179 }
180
181 //////////////////////////////////////////////////////////////////////////////
182 // Setup UserAgentMasterProfile
183 //////////////////////////////////////////////////////////////////////////////
184
185 SharedPtr<UserAgentMasterProfile> profile(new UserAgentMasterProfile);
186
187 // Add transports
188 try
189 {
190 if(mConfig.mUdpPort == (unsigned short)-1
191 #ifdef USE_SSL
192 && mConfig.mTlsPort == (unsigned short)-1
193 #endif
194 && mConfig.mTcpPort == (unsigned short)-1)
195 {
196 // Ensure there is at least one transport enabled - if all are disabled, then enable UDP on an OS selected port
197 mConfig.mUdpPort = 0;
198 }
199 if(mConfig.mUdpPort != (unsigned short)-1)
200 {
201 profile->addTransport(UDP, mConfig.mUdpPort, V4, mConfig.mAddress);
202 if(mIsV6Avail)
203 {
204 profile->addTransport(UDP, mConfig.mUdpPort, V6, mConfig.mAddress);
205 }
206 }
207 if(mConfig.mTcpPort != (unsigned short)-1)
208 {
209 profile->addTransport(TCP, mConfig.mTcpPort, V4, mConfig.mAddress);
210 if(mIsV6Avail)
211 {
212 profile->addTransport(TCP, mConfig.mTcpPort, V6, mConfig.mAddress);
213 }
214 }
215 #ifdef USE_SSL
216 if(mConfig.mTlsPort != (unsigned short)-1)
217 {
218 profile->addTransport(TLS, mConfig.mTlsPort, V4, mConfig.mAddress, mConfig.mTlsDomain);
219 if(mIsV6Avail)
220 {
221 profile->addTransport(TLS, mConfig.mTlsPort, V6, mConfig.mAddress, mConfig.mTlsDomain);
222 }
223 }
224 #endif
225 }
226 catch (BaseException& e)
227 {
228 std::cerr << "Cannot start a transport, likely a port is already in use" << endl;
229 InfoLog (<< "Caught: " << e);
230 exit(-1);
231 }
232
233 // DNS Servers
234 ParseBuffer pb(mConfig.mDnsServers);
235 Data dnsServer;
236 while(!mConfig.mDnsServers.empty() && !pb.eof())
237 {
238 pb.skipWhitespace();
239 const char *start = pb.position();
240 pb.skipToOneOf(ParseBuffer::Whitespace, ";,"); // allow white space
241 pb.data(dnsServer, start);
242 if(DnsUtil::isIpV4Address(dnsServer))
243 {
244 InfoLog( << "Adding DNS Server: " << dnsServer);
245 profile->addAdditionalDnsServer(dnsServer);
246 }
247 else
248 {
249 ErrLog( << "Tried to add dns server, but invalid format: " << dnsServer);
250 }
251 if(!pb.eof())
252 {
253 pb.skipChar();
254 }
255 }
256
257 // Disable Statisitics Manager
258 profile->statisticsManagerEnabled() = false;
259
260 if(mConfig.mKeepAlives)
261 {
262 profile->setKeepAliveTimeForDatagram(30);
263 profile->setKeepAliveTimeForStream(180);
264 }
265
266 // Support Methods, etc.
267 profile->validateContentEnabled() = false;
268 profile->validateContentLanguageEnabled() = false;
269 profile->validateAcceptEnabled() = false;
270
271 profile->clearSupportedLanguages();
272 profile->addSupportedLanguage(Token("en"));
273
274 profile->clearSupportedMimeTypes();
275 profile->addSupportedMimeType(INVITE, Mime("application", "sdp"));
276 profile->addSupportedMimeType(INVITE, Mime("multipart", "mixed"));
277 profile->addSupportedMimeType(INVITE, Mime("multipart", "signed"));
278 profile->addSupportedMimeType(INVITE, Mime("multipart", "alternative"));
279 profile->addSupportedMimeType(OPTIONS,Mime("application", "sdp"));
280 profile->addSupportedMimeType(OPTIONS,Mime("multipart", "mixed"));
281 profile->addSupportedMimeType(OPTIONS, Mime("multipart", "signed"));
282 profile->addSupportedMimeType(OPTIONS, Mime("multipart", "alternative"));
283 profile->addSupportedMimeType(NOTIFY, Mime("message", "sipfrag"));
284
285 profile->clearSupportedMethods();
286 profile->addSupportedMethod(INVITE);
287 profile->addSupportedMethod(ACK);
288 profile->addSupportedMethod(CANCEL);
289 profile->addSupportedMethod(OPTIONS);
290 profile->addSupportedMethod(BYE);
291 profile->addSupportedMethod(REFER);
292 profile->addSupportedMethod(NOTIFY);
293 profile->addSupportedMethod(SUBSCRIBE);
294
295 profile->clearSupportedOptionTags();
296 profile->addSupportedOptionTag(Token(Symbols::Replaces));
297 profile->addSupportedOptionTag(Token(Symbols::Timer));
298 profile->addSupportedOptionTag(Token(Symbols::NoReferSub));
299 profile->addSupportedOptionTag(Token(Symbols::AnswerMode));
300 profile->addSupportedOptionTag(Token(Symbols::TargetDialog));
301
302 profile->setUacReliableProvisionalMode(MasterProfile::Never);
303
304 profile->clearSupportedSchemes();
305 profile->addSupportedScheme("sip");
306 #ifdef USE_SSL
307 profile->addSupportedScheme("sips");
308 #endif
309
310 // Have stack add Allow/Supported/Accept headers to INVITE dialog establishment messages
311 profile->clearAdvertisedCapabilities(); // Remove Profile Defaults, then add our preferences
312 profile->addAdvertisedCapability(Headers::Allow);
313 //profile->addAdvertisedCapability(Headers::AcceptEncoding); // This can be misleading - it might specify what is expected in response
314 profile->addAdvertisedCapability(Headers::AcceptLanguage);
315 profile->addAdvertisedCapability(Headers::Supported);
316 profile->setMethodsParamEnabled(true);
317
318 profile->setUserAgent("MOHParkServer");
319 profile->rtpPortRangeMin() = mConfig.mMediaPortRangeStart;
320 profile->rtpPortRangeMax() = mConfig.mMediaPortRangeStart + mConfig.mMediaPortRangeSize-1;
321
322 // Install Sdp Message Decorator
323 SharedPtr<MessageDecorator> outboundDecorator(new SdpMessageDecorator);
324 profile->setOutboundDecorator(outboundDecorator);
325
326 mUserAgentMasterProfile = profile;
327
328 // Create UserAgent
329 mMyUserAgent = new MyUserAgent(*this, profile, mConfig.mSocketFunc);
330
331 if(mConfig.mHttpPort != 0)
332 {
333 // Create WebAdmin
334 mWebAdmin = new WebAdmin(*this, true /* noWebChallenges */, Data::Empty, Data::Empty, mConfig.mHttpPort, resip::V4);
335 mWebAdminThread = new WebAdminThread(*mWebAdmin);
336 assert(mWebAdminThread && mWebAdmin);
337 mWebAdminThread->run();
338 }
339 }
340
341 Server::~Server()
342 {
343 if(mWebAdminThread)
344 {
345 mWebAdminThread->shutdown();
346 mWebAdminThread->join();
347 delete mWebAdminThread;
348 mWebAdminThread = 0;
349 }
350 if(mWebAdmin)
351 {
352 delete mWebAdmin;
353 mWebAdmin = 0;
354 }
355
356 shutdown();
357 delete mMyUserAgent;
358 }
359
360 void
361 Server::initializeResipLogging(unsigned int maxByteCount, const Data& level, const Data& resipFilename)
362 {
363 // Initialize loggers
364 GenericLogImpl::MaxByteCount = maxByteCount;
365 Log::initialize("file", level.c_str(), "", resipFilename.c_str(), &g_MOHParkServerLogger);
366 }
367
368 void
369 Server::startup()
370 {
371 assert(mMyUserAgent);
372 mMyUserAgent->startup();
373 mMOHManager.startup();
374 mParkManager.startup();
375 }
376
377 void
378 Server::process(int timeoutMs)
379 {
380 assert(mMyUserAgent);
381 mMyUserAgent->process(timeoutMs);
382 }
383
384 void
385 Server::shutdown()
386 {
387 mMOHManager.shutdown(true /*shuttingDownServer*/);
388 mParkManager.shutdown(true /*shuttingDownServer*/);
389 assert(mMyUserAgent);
390 mMyUserAgent->shutdown();
391 OsSysLog::shutdown();
392 }
393
394 void
395 Server::buildSessionCapabilities(resip::SdpContents& sessionCaps)
396 {
397 unsigned int codecIds[] = { SdpCodec::SDP_CODEC_PCMU /* 0 - pcmu */,
398 SdpCodec::SDP_CODEC_PCMA /* 8 - pcma */,
399 SdpCodec::SDP_CODEC_SPEEX /* 96 - speex NB 8,000bps */,
400 SdpCodec::SDP_CODEC_SPEEX_15 /* 98 - speex NB 15,000bps */,
401 SdpCodec::SDP_CODEC_SPEEX_24 /* 99 - speex NB 24,600bps */,
402 SdpCodec::SDP_CODEC_L16_44100_MONO /* PCM 16 bit/sample 44100 samples/sec. */,
403 SdpCodec::SDP_CODEC_G726_16,
404 SdpCodec::SDP_CODEC_G726_24,
405 SdpCodec::SDP_CODEC_G726_32,
406 SdpCodec::SDP_CODEC_G726_40,
407 SdpCodec::SDP_CODEC_ILBC /* 108 - iLBC */,
408 SdpCodec::SDP_CODEC_ILBC_20MS /* 109 - Internet Low Bit Rate Codec, 20ms (RFC3951) */,
409 SdpCodec::SDP_CODEC_SPEEX_5 /* 97 - speex NB 5,950bps */,
410 SdpCodec::SDP_CODEC_GSM /* 3 - GSM */,
411 SdpCodec::SDP_CODEC_TONES /* 110 - telephone-event */};
412 unsigned int numCodecIds = sizeof(codecIds) / sizeof(codecIds[0]);
413 ConversationManager::buildSessionCapabilities(mConfig.mAddress, numCodecIds, codecIds, sessionCaps);
414 }
415
416 void
417 Server::getActiveCallsInfo(std::list<ActiveCallInfo>& callInfos)
418 {
419 callInfos.clear();
420 mMOHManager.getActiveCallsInfo(callInfos);
421 mParkManager.getActiveCallsInfo(callInfos);
422 }
423
424 void
425 Server::onConversationDestroyed(ConversationHandle convHandle)
426 {
427 InfoLog(<< "onConversationDestroyed: handle=" << convHandle);
428 }
429
430 void
431 Server::onParticipantDestroyed(ParticipantHandle partHandle)
432 {
433 InfoLog(<< "onParticipantDestroyed: handle=" << partHandle);
434 if(!mMOHManager.removeParticipant(partHandle))
435 {
436 mParkManager.removeParticipant(partHandle);
437 }
438 }
439
440 void
441 Server::onDtmfEvent(ParticipantHandle partHandle, int dtmf, int duration, bool up)
442 {
443 InfoLog(<< "onDtmfEvent: handle=" << partHandle << " tone=" << dtmf << " dur=" << duration << " up=" << up);
444 }
445
446 void
447 Server::onIncomingParticipant(ParticipantHandle partHandle, const SipMessage& msg, bool autoAnswer, ConversationProfile& conversationProfile)
448 {
449 InfoLog(<< "onIncomingParticipant: handle=" << partHandle << " auto=" << autoAnswer << " msg=" << msg.brief());
450
451 if(mMOHManager.isMyProfile(conversationProfile))
452 {
453 mMOHManager.addParticipant(partHandle, msg.header(h_From).uri(), msg.header(h_From).uri());
454 }
455 else if(mParkManager.isMyProfile(conversationProfile))
456 {
457 mParkManager.incomingParticipant(partHandle, msg);
458 }
459 else
460 {
461 rejectParticipant(partHandle, 404);
462 }
463 }
464
465 void
466 Server::onRequestOutgoingParticipant(ParticipantHandle partHandle, const SipMessage& msg, ConversationProfile& conversationProfile)
467 {
468 InfoLog(<< "onRequestOutgoingParticipant: handle=" << partHandle << " msg=" << msg.brief());
469 if(mMOHManager.isMyProfile(conversationProfile))
470 {
471 mMOHManager.addParticipant(partHandle, msg.header(h_ReferTo).uri().getAorAsUri(), msg.header(h_From).uri());
472 }
473 else if(mParkManager.isMyProfile(conversationProfile))
474 {
475 mParkManager.parkParticipant(partHandle, msg);
476 }
477 else
478 {
479 rejectParticipant(partHandle, 404);
480 }
481 }
482
483 void
484 Server::onParticipantTerminated(ParticipantHandle partHandle, unsigned int statusCode)
485 {
486 InfoLog(<< "onParticipantTerminated: handle=" << partHandle);
487 }
488
489 void
490 Server::onParticipantProceeding(ParticipantHandle partHandle, const SipMessage& msg)
491 {
492 InfoLog(<< "onParticipantProceeding: handle=" << partHandle << " msg=" << msg.brief());
493 }
494
495 void
496 Server::onRelatedConversation(ConversationHandle relatedConvHandle, ParticipantHandle relatedPartHandle,
497 ConversationHandle origConvHandle, ParticipantHandle origPartHandle)
498 {
499 InfoLog(<< "onRelatedConversation: relatedConvHandle=" << relatedConvHandle << " relatedPartHandle=" << relatedPartHandle
500 << " origConvHandle=" << origConvHandle << " origPartHandle=" << origPartHandle);
501 }
502
503 void
504 Server::onParticipantAlerting(ParticipantHandle partHandle, const SipMessage& msg)
505 {
506 InfoLog(<< "onParticipantAlerting: handle=" << partHandle << " msg=" << msg.brief());
507 }
508
509 void
510 Server::onParticipantConnected(ParticipantHandle partHandle, const SipMessage& msg)
511 {
512 InfoLog(<< "onParticipantConnected: handle=" << partHandle << " msg=" << msg.brief());
513 }
514
515 void
516 Server::onParticipantRedirectSuccess(ParticipantHandle partHandle)
517 {
518 InfoLog(<< "onParticipantRedirectSuccess: handle=" << partHandle);
519 destroyParticipant(partHandle); // Transfer is successful - end participant
520 }
521
522 void
523 Server::onParticipantRedirectFailure(ParticipantHandle partHandle, unsigned int statusCode)
524 {
525 InfoLog(<< "onParticipantRedirectFailure: handle=" << partHandle << " statusCode=" << statusCode);
526 }
527
528 void
529 Server::onMaxParkTimeout(recon::ParticipantHandle participantHandle)
530 {
531 // Pass to ParkManager to see if participant is still around
532 mParkManager.onMaxParkTimeout(participantHandle);
533 }
534
535 }
536
537 /* ====================================================================
538
539 Copyright (c) 2010, SIP Spectrum, Inc.
540 All rights reserved.
541
542 Redistribution and use in source and binary forms, with or without
543 modification, are permitted provided that the following conditions are
544 met:
545
546 1. Redistributions of source code must retain the above copyright
547 notice, this list of conditions and the following disclaimer.
548
549 2. Redistributions in binary form must reproduce the above copyright
550 notice, this list of conditions and the following disclaimer in the
551 documentation and/or other materials provided with the distribution.
552
553 3. Neither the name of SIP Spectrum nor the names of its contributors
554 may be used to endorse or promote products derived from this
555 software without specific prior written permission.
556
557 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
558 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
559 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
560 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
561 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
562 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
563 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
564 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
565 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
566 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
567 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
568
569 ==================================================================== */
570

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