|
reSIProcate/rutil
9694
|
00001 #ifdef HAVE_CONFIG_H 00002 #include "config.h" 00003 #endif 00004 00005 #if !defined(WIN32) 00006 #include <sys/types.h> 00007 #endif 00008 #include <time.h> 00009 00010 #include "rutil/dns/AresDns.hxx" 00011 #include "rutil/GenericIPAddress.hxx" 00012 00013 #include "AresCompat.hxx" 00014 #if !defined(USE_CARES) 00015 #include "ares_private.h" 00016 #endif 00017 00018 #include "rutil/Logger.hxx" 00019 #include "rutil/DnsUtil.hxx" 00020 #include "rutil/WinLeakCheck.hxx" 00021 #include "rutil/FdPoll.hxx" 00022 00023 #if !defined(WIN32) 00024 #if !defined(__CYGWIN__) 00025 #include <arpa/nameser.h> 00026 #endif 00027 #endif 00028 00029 using namespace resip; 00030 00031 #define RESIPROCATE_SUBSYSTEM resip::Subsystem::DNS 00032 00033 /********************************************************************** 00034 * 00035 * class AresDnsPollItem 00036 * 00037 * This is callback class used for epoll-based systems. 00038 * 00039 **********************************************************************/ 00040 00041 #ifndef USE_CARES 00042 namespace resip 00043 { 00044 00045 class AresDnsPollItem : public FdPollItemBase 00046 { 00047 public: 00048 AresDnsPollItem(FdPollGrp *grp, int fd, AresDns& aresObj, 00049 ares_channel chan, int server_idx) 00050 : FdPollItemBase(grp, fd, FPEM_Read), mAres(aresObj), 00051 mChannel(chan), mFd(fd), mServerIdx(server_idx) 00052 { 00053 } 00054 00055 virtual void processPollEvent(FdPollEventMask mask); 00056 void resetPollGrp(FdPollGrp *grp) 00057 { 00058 mPollGrp->delPollItem(mPollHandle); 00059 mPollGrp = grp; 00060 mPollHandle = mPollGrp->addPollItem(mFd, FPEM_Read, this); 00061 } 00062 00063 AresDns& mAres; 00064 ares_channel mChannel; 00065 int mFd; 00066 int mServerIdx; 00067 00068 static void socket_poll_cb(void *cb_data, 00069 ares_channel channel, int server_idx, 00070 int fd, ares_poll_action_t act); 00071 }; 00072 00073 }; 00074 00075 void 00076 AresDnsPollItem::processPollEvent(FdPollEventMask mask) 00077 { 00078 assert( (mask&(FPEM_Read|FPEM_Write))!= 0 ); 00079 00080 time_t nowSecs; 00081 time(&nowSecs); 00082 00083 ares_process_poll(mChannel, mServerIdx, 00084 (mask&FPEM_Read)?(int)mPollSocket:-1, (mask&FPEM_Write)?(int)mPollSocket:-1, 00085 nowSecs); 00086 } 00087 00093 void 00094 AresDnsPollItem::socket_poll_cb(void *cb_data, 00095 ares_channel channel, int server_idx, 00096 int fd, ares_poll_action_t act) 00097 { 00098 AresDns *ares = static_cast<AresDns*>(cb_data); 00099 //assert( ares ); 00100 FdPollGrp *grp = ares->mPollGrp; 00101 //assert( grp ); 00102 AresDnsPollItem *olditem = ares->mPollItems.at(server_idx); 00103 if ( olditem ) 00104 { 00105 assert( olditem->mChannel==channel ); 00106 assert( olditem->mServerIdx==server_idx ); 00107 } 00108 switch ( act ) 00109 { 00110 case ARES_POLLACTION_OPEN: 00111 assert( olditem==NULL ); 00112 assert( fd!=INVALID_SOCKET ); 00113 ares->mPollItems[server_idx] = new AresDnsPollItem( grp, fd, *ares, channel, server_idx); 00114 break; 00115 case ARES_POLLACTION_CLOSE: 00116 assert( olditem ); 00117 ares->mPollItems[server_idx] = NULL; 00118 delete olditem; // destructor removes from poll 00119 break; 00120 case ARES_POLLACTION_WRITEON: 00121 assert( olditem ); 00122 grp->modPollItem(olditem->mPollHandle, FPEM_Read|FPEM_Write); 00123 break; 00124 case ARES_POLLACTION_WRITEOFF: 00125 assert( olditem ); 00126 grp->modPollItem(olditem->mPollHandle, FPEM_Read); 00127 break; 00128 default: 00129 assert( 0 ); 00130 } 00131 } 00132 00133 #endif 00134 00135 /********************************************************************** 00136 * 00137 * class AresDns 00138 * 00139 **********************************************************************/ 00140 00141 volatile bool AresDns::mHostFileLookupOnlyMode = false; 00142 00143 void 00144 AresDns::setPollGrp(FdPollGrp *grp) 00145 { 00146 #ifdef USE_CARES 00147 if(mPollGrp) 00148 { 00149 mPollGrp->unregisterFdSetIOObserver(*this); 00150 } 00151 mPollGrp=grp; 00152 if(mPollGrp) 00153 { 00154 mPollGrp->registerFdSetIOObserver(*this); 00155 } 00156 #else 00157 for(std::vector<AresDnsPollItem*>::iterator i=mPollItems.begin(); 00158 i!=mPollItems.end(); ++i) 00159 { 00160 if(*i) 00161 { 00162 (*i)->resetPollGrp(grp); 00163 } 00164 } 00165 mPollGrp = grp; 00166 #endif 00167 } 00168 00169 int 00170 AresDns::init(const std::vector<GenericIPAddress>& additionalNameservers, 00171 AfterSocketCreationFuncPtr socketfunc, 00172 int timeout, 00173 int tries, 00174 unsigned int features) 00175 { 00176 mAdditionalNameservers = additionalNameservers; 00177 mFeatures = features; 00178 00179 int ret = internalInit(additionalNameservers, 00180 socketfunc, 00181 features, 00182 &mChannel, 00183 timeout, 00184 tries); 00185 00186 if (ret != Success) 00187 return ret; 00188 00189 #ifdef WIN32 00190 // For windows OSs it is uncommon to run a local DNS server. Therefor if there 00191 // are no defined DNS servers in windows networking and ARES just returned the 00192 // loopback address (ie. default localhost server / named) 00193 // then put resip DNS resolution into hostfile lookup only mode 00194 if(mChannel->nservers == 1 && 00195 mChannel->servers[0].default_localhost_server) 00196 { 00197 // enable hostfile only lookup mode 00198 mHostFileLookupOnlyMode = true; 00199 } 00200 else 00201 { 00202 // disable hostfile only lookup mode 00203 mHostFileLookupOnlyMode = false; 00204 } 00205 #endif 00206 00207 return Success; 00208 } 00209 00210 int 00211 AresDns::internalInit(const std::vector<GenericIPAddress>& additionalNameservers, 00212 AfterSocketCreationFuncPtr socketfunc, 00213 unsigned int features, 00214 ares_channeldata** channel, 00215 int timeout, 00216 int tries 00217 ) 00218 { 00219 if(*channel) 00220 { 00221 #if defined(USE_ARES) 00222 ares_destroy_suppress_callbacks(*channel); 00223 #elif defined(USE_CARES) 00224 // Callbacks will be supressed by looking for the ARES_EDESTRUCTION 00225 // sentinel status 00226 ares_destroy(*channel); 00227 #endif 00228 *channel = 0; 00229 } 00230 00231 #if defined(USE_ARES) 00232 00233 #ifdef USE_IPV6 00234 int requiredCap = ARES_CAP_IPV6; 00235 #else 00236 int requiredCap = 0; 00237 #endif 00238 00239 // Only the contrib/ares has this function 00240 int cap = ares_capabilities(requiredCap); 00241 if (cap != requiredCap) 00242 { 00243 ErrLog (<< "Build mismatch (ipv4/ipv6) problem in ares library"); // !dcm! 00244 return BuildMismatch; 00245 } 00246 #endif 00247 00248 int status; 00249 ares_options opt; 00250 int optmask = 0; 00251 00252 memset(&opt, '\0', sizeof(opt)); 00253 00254 #if defined(USE_ARES) 00255 // TODO: What is this and how does it map to c-ares? 00256 if ((features & ExternalDns::TryServersOfNextNetworkUponRcode3)) 00257 { 00258 optmask |= ARES_OPT_FLAGS; 00259 opt.flags |= ARES_FLAG_TRY_NEXT_SERVER_ON_RCODE3; 00260 } 00261 #endif 00262 00263 #if defined(USE_CARES) 00264 // In c-ares, we can actually set the timeout and retries via the API 00265 if (timeout > 0) 00266 { 00267 opt.timeout = timeout; 00268 optmask |= ARES_OPT_TIMEOUT; 00269 } 00270 00271 if (tries > 0) 00272 { 00273 opt.tries = tries; 00274 optmask |= ARES_OPT_TRIES; 00275 } 00276 #endif 00277 00278 if (additionalNameservers.empty()) 00279 { 00280 #if defined(USE_ARES) 00281 status = ares_init_options_with_socket_function(channel, &opt, optmask, socketfunc); 00282 #elif defined(USE_CARES) 00283 // TODO: Does the socket function matter? 00284 status = ares_init_options(channel, &opt, optmask); 00285 #endif 00286 } 00287 else 00288 { 00289 optmask |= ARES_OPT_SERVERS; 00290 opt.nservers = (int)additionalNameservers.size(); 00291 00292 #if defined(USE_IPV6) && defined(USE_ARES) 00293 // With contrib/ares, you can configure IPv6 addresses for the 00294 // nameservers themselves. 00295 opt.servers = new multiFamilyAddr[additionalNameservers.size()]; 00296 for (size_t i =0; i < additionalNameservers.size(); i++) 00297 { 00298 if (additionalNameservers[i].isVersion4()) 00299 { 00300 opt.servers[i].family = AF_INET; 00301 opt.servers[i].addr = additionalNameservers[i].v4Address.sin_addr; 00302 } 00303 else 00304 { 00305 opt.servers[i].family = AF_INET6; 00306 opt.servers[i].addr6 = additionalNameservers[i].v6Address.sin6_addr; 00307 } 00308 } 00309 #else 00310 // If we're only supporting IPv4 or we are using c-ares, we can't 00311 // support additional nameservers that are IPv6 right now. 00312 opt.servers = new in_addr[additionalNameservers.size()]; 00313 for (size_t i =0; i < additionalNameservers.size(); i++) 00314 { 00315 if(additionalNameservers[i].isVersion4()) 00316 { 00317 opt.servers[i] = additionalNameservers[i].v4Address.sin_addr; 00318 } 00319 else 00320 { 00321 #if defined(USE_CARES) 00322 WarningLog (<< "Ignoring non-IPv4 additional name server (not yet supported with c-ares)"); 00323 #elif defined(USE_ARES) 00324 WarningLog (<< "Ignoring non-IPv4 additional name server (IPv6 support was not enabled)"); 00325 #endif 00326 } 00327 } 00328 #endif 00329 00330 #if defined(USE_ARES) 00331 status = ares_init_options_with_socket_function(channel, &opt, optmask, socketfunc); 00332 #elif defined(USE_CARES) 00333 // TODO: Does the socket function matter? 00334 status = ares_init_options(channel, &opt, optmask); 00335 #endif 00336 00337 delete [] opt.servers; 00338 opt.servers = 0; 00339 } 00340 if (status != ARES_SUCCESS) 00341 { 00342 ErrLog (<< "Failed to initialize DNS library (status=" << status << ")"); 00343 return status; 00344 } 00345 else 00346 { 00347 00348 #if defined(USE_ARES) 00349 00350 InfoLog(<< "DNS initialization: found " << (*channel)->nservers << " name servers"); 00351 for (int i = 0; i < (*channel)->nservers; ++i) 00352 { 00353 #ifdef USE_IPV6 00354 if((*channel)->servers[i].family == AF_INET6) 00355 { 00356 InfoLog(<< " name server: " << DnsUtil::inet_ntop((*channel)->servers[i].addr6)); 00357 } 00358 else 00359 #endif 00360 { 00361 InfoLog(<< " name server: " << DnsUtil::inet_ntop((*channel)->servers[i].addr)); 00362 } 00363 } 00364 00365 // In ares, we must manipulate these directly 00366 if (timeout > 0) 00367 { 00368 mChannel->timeout = timeout; 00369 } 00370 00371 if (tries > 0) 00372 { 00373 mChannel->tries = tries; 00374 } 00375 00376 #ifndef USE_CARES 00377 if ( mPollGrp ) 00378 { 00379 // expand vector to hold {nservers} and init to NULL 00380 mPollItems.insert( mPollItems.end(), (*channel)->nservers, (AresDnsPollItem*)0); 00381 // tell ares to let us know when things change 00382 ares_process_set_poll_cb(mChannel, AresDnsPollItem::socket_poll_cb, this); 00383 } 00384 #endif 00385 00386 #elif defined(USE_CARES) 00387 { 00388 // Log which version of c-ares we're using 00389 InfoLog(<< "DNS initialization: using c-ares v" 00390 << ::ares_version(NULL)); 00391 00392 // Ask for the current configuration so we can print the servers found 00393 struct ares_options options; 00394 std::memset(&options, 0, sizeof(options)); 00395 int ignored; 00396 if(ares_save_options(*channel, &options, &ignored) == ARES_SUCCESS) 00397 { 00398 InfoLog(<< "DNS initialization: found " 00399 << options.nservers << " name servers"); 00400 00401 // Log them all 00402 for (int i = 0; i < options.nservers; ++i) 00403 { 00404 InfoLog(<< " name server: " 00405 << DnsUtil::inet_ntop(options.servers[i])); 00406 } 00407 ares_destroy_options(&options); 00408 } 00409 } 00410 #endif 00411 00412 return Success; 00413 } 00414 } 00415 00416 bool AresDns::checkDnsChange() 00417 { 00418 // We must return 'true' if there are changes in the list of DNS servers 00419 struct ares_channeldata* channel = 0; 00420 bool bRet = false; 00421 int result = internalInit(mAdditionalNameservers, 0, mFeatures, &channel); 00422 if(result != Success || channel == 0) 00423 { 00424 // It has changed because it failed, I suppose 00425 InfoLog(<< " DNS server list changed"); 00426 return true; 00427 } 00428 00429 #if defined(USE_ARES) 00430 { 00431 // Compare the two lists. Are they different sizes? 00432 if(mChannel->nservers != channel->nservers) 00433 { 00434 // Yes, so they're different 00435 bRet = true; 00436 } 00437 else 00438 { 00439 // Compare them one-by-one 00440 for (int i = 0; i < mChannel->nservers; ++i) 00441 { 00442 if (mChannel->servers[i].addr.s_addr 00443 != channel->servers[i].addr.s_addr) 00444 { 00445 bRet = true; 00446 break; 00447 } 00448 } 00449 } 00450 00451 // Destroy the secondary configuration we read 00452 ares_destroy_suppress_callbacks(channel); 00453 } 00454 #elif defined(USE_CARES) 00455 { 00456 // Get the options, including the server list, from the old and the 00457 // current (i.e. just read) configuration. 00458 struct ares_options old; 00459 struct ares_options updated; 00460 std::memset(&old, 0, sizeof(old)); 00461 std::memset(&updated, 0, sizeof(updated)); 00462 int ignored; 00463 00464 // Can we get the configuration? 00465 if(ares_save_options(mChannel, &old, &ignored) != ARES_SUCCESS 00466 || ares_save_options(channel, &updated, &ignored) != ARES_SUCCESS) 00467 { 00468 // It failed, so call it different 00469 bRet = true; 00470 } 00471 else 00472 { 00473 // Compare the two lists. Are they different sizes? 00474 if(old.nservers != updated.nservers) 00475 { 00476 // Yes, so they're different 00477 bRet = true; 00478 } 00479 else 00480 { 00481 // Compare them one-by-one 00482 for (int i = 0; i < old.nservers; ++i) 00483 { 00484 if (old.servers[i].s_addr != updated.servers[i].s_addr) 00485 { 00486 bRet = true; 00487 break; 00488 } 00489 } 00490 } 00491 00492 // Free any ares_options contents we have created. 00493 ares_destroy_options(&old); 00494 ares_destroy_options(&updated); 00495 } 00496 00497 // Destroy the secondary configuration we read 00498 ares_destroy(channel); 00499 } 00500 #endif 00501 00502 // Report on the results 00503 if(!bRet) 00504 { 00505 InfoLog(<< " No changes in DNS server list"); 00506 } 00507 else 00508 { 00509 InfoLog(<< " DNS server list changed"); 00510 } 00511 00512 return bRet; 00513 } 00514 00515 AresDns::~AresDns() 00516 { 00517 #if defined(USE_ARES) 00518 ares_destroy_suppress_callbacks(mChannel); 00519 #elif defined(USE_CARES) 00520 ares_destroy(mChannel); 00521 #endif 00522 } 00523 00524 bool AresDns::hostFileLookup(const char* target, in_addr &addr) 00525 { 00526 assert(target); 00527 00528 hostent *hostdata = 0; 00529 00530 // Look this up 00531 int status = 00532 #if defined(USE_ARES) 00533 hostfile_lookup(target, &hostdata) 00534 #elif defined(USE_CARES) 00535 ares_gethostbyname_file(mChannel, target, AF_INET, &hostdata) 00536 #endif 00537 ; 00538 00539 if (status != ARES_SUCCESS) 00540 { 00541 DebugLog(<< "hostFileLookup failed for " << target); 00542 return false; 00543 } 00544 sockaddr_in saddr; 00545 memset(&saddr,0,sizeof(saddr)); /* Initialize sockaddr fields. */ 00546 saddr.sin_family = AF_INET; 00547 memcpy((char *)&(saddr.sin_addr.s_addr),(char *)hostdata->h_addr_list[0], (size_t)hostdata->h_length); 00548 addr = saddr.sin_addr; 00549 #if defined(USE_ARES) 00550 // for resip-ares, the hostdata (and its contents) is dynamically allocated 00551 ares_free_hostent(hostdata); 00552 #endif 00553 00554 DebugLog(<< "hostFileLookup succeeded for " << target); 00555 return true; 00556 } 00557 00558 ExternalDnsHandler* 00559 AresDns::getHandler(void* arg) 00560 { 00561 Payload* p = reinterpret_cast<Payload*>(arg); 00562 ExternalDnsHandler *thisp = reinterpret_cast<ExternalDnsHandler*>(p->first); 00563 return thisp; 00564 } 00565 00566 ExternalDnsRawResult 00567 AresDns::makeRawResult(void *arg, int status, unsigned char *abuf, int alen) 00568 { 00569 Payload* p = reinterpret_cast<Payload*>(arg); 00570 void* userArg = reinterpret_cast<void*>(p->second); 00571 00572 if (status != ARES_SUCCESS) 00573 { 00574 return ExternalDnsRawResult(status, abuf, alen, userArg); 00575 } 00576 else 00577 { 00578 return ExternalDnsRawResult(abuf, alen, userArg); 00579 } 00580 } 00581 00582 unsigned int 00583 AresDns::getTimeTillNextProcessMS() 00584 { 00585 struct timeval tv; 00586 ares_timeout(mChannel, NULL, &tv); 00587 return tv.tv_sec*1000 + tv.tv_usec / 1000; 00588 } 00589 00590 void 00591 AresDns::buildFdSet(fd_set& read, fd_set& write, int& size) 00592 { 00593 int newsize = ares_fds(mChannel, &read, &write); 00594 if ( newsize > size ) 00595 { 00596 size = newsize; 00597 } 00598 } 00599 00600 void 00601 AresDns::processTimers() 00602 { 00603 #ifdef USE_CARES 00604 return; 00605 #else 00606 assert( mPollGrp!=0 ); 00607 time_t timeSecs; 00608 time(&timeSecs); 00609 ares_process_poll(mChannel, /*server*/-1, /*rd*/-1, /*wr*/-1, timeSecs); 00610 #endif 00611 } 00612 00613 void 00614 AresDns::process(FdSet& fdset) 00615 { 00616 process(fdset.read, fdset.write); 00617 } 00618 00619 void 00620 AresDns::buildFdSet(FdSet& fdset) 00621 { 00622 buildFdSet(fdset.read, fdset.write, fdset.size); 00623 } 00624 00625 void 00626 AresDns::process(fd_set& read, fd_set& write) 00627 { 00628 ares_process(mChannel, &read, &write); 00629 } 00630 00631 char* 00632 AresDns::errorMessage(long errorCode) 00633 { 00634 const char* aresMsg = ares_strerror(errorCode); 00635 00636 size_t len = strlen(aresMsg); 00637 char* errorString = new char[len+1]; 00638 00639 strncpy(errorString, aresMsg, len); 00640 errorString[len] = '\0'; 00641 return errorString; 00642 } 00643 00644 void 00645 AresDns::lookup(const char* target, unsigned short type, ExternalDnsHandler* handler, void* userData) 00646 { 00647 ares_query(mChannel, target, C_IN, type, 00648 #if defined(USE_ARES) 00649 resip_AresDns_aresCallback, 00650 #elif defined(USE_CARES) 00651 resip_AresDns_caresCallback, 00652 #endif 00653 new Payload(handler, userData)); 00654 } 00655 00656 void 00657 resip_AresDns_aresCallback(void *arg, int status, unsigned char *abuf, int alen) 00658 { 00659 #if defined(USE_CARES) 00660 // If this is destruction, skip it. We do this here for completeness. 00661 if(status == ARES_EDESTRUCTION) 00662 { 00663 return; 00664 } 00665 #endif 00666 00667 resip::AresDns::getHandler(arg)->handleDnsRaw(resip::AresDns::makeRawResult(arg, status, abuf, alen)); 00668 resip::AresDns::Payload* p = reinterpret_cast<resip::AresDns::Payload*>(arg); 00669 delete p; 00670 } 00671 00672 void 00673 resip_AresDns_caresCallback(void *arg, int status, int timeouts, 00674 unsigned char *abuf, int alen) 00675 { 00676 // Simply ignore the timeouts argument 00677 return ::resip_AresDns_aresCallback(arg, status, abuf, alen); 00678 } 00679 00680 /* ==================================================================== 00681 * The Vovida Software License, Version 1.0 00682 * 00683 * Copyright (c) 2000-2005 Vovida Networks, Inc. All rights reserved. 00684 * 00685 * Redistribution and use in source and binary forms, with or without 00686 * modification, are permitted provided that the following conditions 00687 * are met: 00688 * 00689 * 1. Redistributions of source code must retain the above copyright 00690 * notice, this list of conditions and the following disclaimer. 00691 * 00692 * 2. Redistributions in binary form must reproduce the above copyright 00693 * notice, this list of conditions and the following disclaimer in 00694 * the documentation and/or other materials provided with the 00695 * distribution. 00696 * 00697 * 3. The names "VOCAL", "Vovida Open Communication Application Library", 00698 * and "Vovida Open Communication Application Library (VOCAL)" must 00699 * not be used to endorse or promote products derived from this 00700 * software without prior written permission. For written 00701 * permission, please contact vocal@vovida.org. 00702 * 00703 * 4. Products derived from this software may not be called "VOCAL", nor 00704 * may "VOCAL" appear in their name, without prior written 00705 * permission of Vovida Networks, Inc. 00706 * 00707 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED 00708 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 00709 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND 00710 * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL VOVIDA 00711 * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES 00712 * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL, 00713 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 00714 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 00715 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 00716 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 00717 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 00718 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 00719 * DAMAGE. 00720 * 00721 * ==================================================================== 00722 * 00723 * This software consists of voluntary contributions made by Vovida 00724 * Networks, Inc. and many individuals on behalf of Vovida Networks, 00725 * Inc. For more information on Vovida Networks, Inc., please see 00726 * <http://www.vovida.org/>. 00727 * 00728 * vi: shiftwidth=3 expandtab: 00729 */
1.7.5.1