source: branches/nat-traversal/libtransmission/natpmp.c @ 864

Last change on this file since 864 was 864, checked in by joshe, 16 years ago

Add nat traversal support (ie: NAT-PMP and UPnP)

  • Property svn:keywords set to Date Rev Author Id
File size: 20.5 KB
Line 
1/******************************************************************************
2 * $Id: natpmp.c 864 2006-09-17 23:00:51Z joshe $
3 *
4 * Copyright (c) 2006 Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25#include "transmission.h"
26
27#define PMP_PORT                5351
28#define PMP_MCAST_ADDR          "224.0.0.1"
29#define PMP_INITIAL_DELAY       250     /* ms, 1/4 second */
30#define PMP_TOTAL_DELAY         120000  /* ms, 2 minutes */
31#define PMP_VERSION             0
32#define PMP_OPCODE_GETIP        0
33#define PMP_OPCODE_ADDUDP       1
34#define PMP_OPCODE_ADDTCP       2
35#define PMP_LIFETIME            3600    /* secs, one hour */
36#define PMP_RESPCODE_OK         0
37#define PMP_RESPCODE_BADVERS    1
38#define PMP_RESPCODE_REFUSED    2
39#define PMP_RESPCODE_NETDOWN    3
40#define PMP_RESPCODE_NOMEM      4
41#define PMP_RESPCODE_BADOPCODE  5
42
43#define PMP_OPCODE_FROM_RESPONSE( op )  ( 0x80 ^ (op) )
44#define PMP_OPCODE_TO_RESPONSE( op )    ( 0x80 | (op) )
45#define PMP_OPCODE_IS_RESPONSE( op )    ( 0x80 & (op) )
46#define PMP_TOBUF16( buf, num ) ( *( (uint16_t *) (buf) ) = htons( (num) ) )
47#define PMP_TOBUF32( buf, num ) ( *( (uint32_t *) (buf) ) = htonl( (num) ) )
48#define PMP_FROMBUF16( buf )    ( htons( *( (uint16_t *) (buf) ) ) )
49#define PMP_FROMBUF32( buf )    ( htonl( *( (uint32_t *) (buf) ) ) )
50
51typedef struct tr_natpmp_uptime_s
52{
53    time_t   when;
54    uint32_t uptime;
55} tr_natpmp_uptime_t;
56
57typedef struct tr_natpmp_req_s
58{
59    unsigned int         adding : 1;
60    unsigned int         nobodyhome : 1;
61    int                  fd;
62    int                  delay;
63    uint64_t             retry;
64    uint64_t             timeout;
65    int                  port;
66    tr_fd_t *            fdlimit;
67    tr_natpmp_uptime_t * uptime;
68} tr_natpmp_req_t;
69
70struct tr_natpmp_s
71{
72#define PMP_STATE_IDLE          1
73#define PMP_STATE_ADDING        2
74#define PMP_STATE_DELETING      3
75#define PMP_STATE_MAPPED        4
76#define PMP_STATE_FAILED        5
77#define PMP_STATE_NOBODYHOME    6
78    char               state;
79    unsigned int       active : 1;
80    struct in_addr     dest;
81    int                newport;
82    int                mappedport;
83    tr_fd_t         *  fdlimit;
84    tr_lock_t          lock;
85    uint64_t           renew;
86    tr_natpmp_req_t *  req;
87    tr_natpmp_uptime_t uptime;
88    int                mcastfd;
89};
90
91static int
92checktime( tr_natpmp_uptime_t * uptime, uint32_t seen );
93static void
94killsock( int * fd, tr_fd_t * fdlimit );
95static tr_natpmp_req_t *
96newreq( int adding, struct in_addr addr, int port, tr_fd_t * fdlimit,
97        tr_natpmp_uptime_t * uptime );
98static tr_tristate_t
99pulsereq( tr_natpmp_req_t * req, uint64_t * renew );
100static int
101mcastsetup( tr_fd_t * fdlimit );
102static void
103mcastpulse( tr_natpmp_t * pmp );
104static void
105killreq( tr_natpmp_req_t ** req );
106static int
107sendrequest( int adding, int fd, int port );
108static tr_tristate_t
109readrequest( uint8_t * buf, int len, int adding, int port,
110             tr_natpmp_uptime_t * uptime, uint64_t * renew );
111
112tr_natpmp_t *
113tr_natpmpInit( tr_fd_t * fdlimit )
114{
115    tr_natpmp_t * pmp;
116
117    pmp = calloc( 1, sizeof( *pmp ) );
118    if( NULL == pmp )
119    {
120        return NULL;
121    }
122
123    pmp->state       = PMP_STATE_IDLE;
124    pmp->fdlimit     = fdlimit;
125    pmp->mcastfd     = -1;
126
127    if( tr_getDefaultRoute( &pmp->dest ) || INADDR_ANY == pmp->dest.s_addr )
128    {
129        pmp->dest.s_addr = INADDR_NONE;
130    }
131
132    if( INADDR_NONE == pmp->dest.s_addr )
133    {
134        tr_dbg( "nat-pmp device is unknown" );
135    }
136    else
137    {
138        char addrstr[INET_ADDRSTRLEN];
139        tr_netNtop( &pmp->dest, addrstr, sizeof( addrstr ) );
140        tr_dbg( "nat-pmp device is %s", addrstr );
141    }
142
143    tr_lockInit( &pmp->lock );
144
145    return pmp;
146}
147
148void
149tr_natpmpStart( tr_natpmp_t * pmp )
150{
151    tr_lockLock( &pmp->lock );
152
153    if( !pmp->active )
154    {
155        tr_inf( "starting nat-pmp" );
156        pmp->active = 1;
157        if( 0 > pmp->mcastfd )
158        {
159            pmp->mcastfd = mcastsetup( pmp->fdlimit );
160        }
161        /* XXX should I change state? */
162    }
163
164    tr_lockUnlock( &pmp->lock );
165}
166
167void
168tr_natpmpStop( tr_natpmp_t * pmp )
169{
170    tr_lockLock( &pmp->lock );
171
172    if( pmp->active )
173    {
174        tr_inf( "stopping nat-pmp" );
175        pmp->active = 0;
176        killsock( &pmp->mcastfd, pmp->fdlimit );
177        switch( pmp->state )
178        {
179            case PMP_STATE_IDLE:
180                break;
181            case PMP_STATE_ADDING:
182                pmp->state = PMP_STATE_IDLE;
183                tr_dbg( "nat-pmp state add -> idle" );
184                if( NULL != pmp->req )
185                {
186                    killreq( &pmp->req );
187                    pmp->mappedport = pmp->req->port;
188                    pmp->state = PMP_STATE_DELETING;
189                    tr_dbg( "nat-pmp state idle -> del" );
190                }
191                break;
192            case PMP_STATE_DELETING:
193                break;
194            case PMP_STATE_MAPPED:
195                pmp->state = PMP_STATE_DELETING;
196                tr_dbg( "nat-pmp state mapped -> del" );
197                break;
198            case PMP_STATE_FAILED:
199            case PMP_STATE_NOBODYHOME:
200                break;
201            default:
202                assert( 0 );
203                break;
204        }
205    }
206
207    tr_lockUnlock( &pmp->lock );
208}
209
210int
211tr_natpmpStatus( tr_natpmp_t * pmp )
212{
213    int ret;
214
215    tr_lockLock( &pmp->lock );
216
217   
218    if( !pmp->active )
219    {
220        ret = ( PMP_STATE_DELETING == pmp->state ?
221                TR_NAT_TRAVERSAL_UNMAPPING : TR_NAT_TRAVERSAL_DISABLED );
222    }
223    else if( 0 < pmp->mappedport )
224    {
225        ret = TR_NAT_TRAVERSAL_MAPPED;
226    }
227    else
228    {
229        switch( pmp->state )
230        {
231            case PMP_STATE_IDLE:
232            case PMP_STATE_ADDING:
233            case PMP_STATE_DELETING:
234                ret = TR_NAT_TRAVERSAL_MAPPING;
235                break;
236            case PMP_STATE_FAILED:
237                ret = TR_NAT_TRAVERSAL_ERROR;
238                break;
239            case PMP_STATE_NOBODYHOME:
240                ret = TR_NAT_TRAVERSAL_NOTFOUND;
241                break;
242            case PMP_STATE_MAPPED:
243            default:
244                assert( 0 );
245                ret = TR_NAT_TRAVERSAL_ERROR;
246                break;
247        }
248    }
249
250    tr_lockUnlock( &pmp->lock );
251
252    return ret;
253}
254
255void
256tr_natpmpForwardPort( tr_natpmp_t * pmp, int port )
257{
258    tr_lockLock( &pmp->lock );
259    tr_inf( "nat-pmp set port %i", port );
260    pmp->newport = port;
261    tr_lockUnlock( &pmp->lock );
262}
263
264void
265tr_natpmpClose( tr_natpmp_t * pmp )
266{
267    /* try to send at least one delete request if we have a port mapping */
268    tr_natpmpStop( pmp );
269    tr_natpmpPulse( pmp );
270
271    tr_lockLock( &pmp->lock );
272    killreq( &pmp->req );
273    tr_lockClose( &pmp->lock );
274    free( pmp );
275}
276
277void
278tr_natpmpPulse( tr_natpmp_t * pmp )
279{
280    tr_lockLock( &pmp->lock );
281
282    if( 0 <= pmp->mcastfd )
283    {
284        mcastpulse( pmp );
285    }
286
287    if( pmp->active || PMP_STATE_DELETING == pmp->state )
288    {
289        switch( pmp->state )
290        {
291            case PMP_STATE_IDLE:
292                if( 0 < pmp->newport )
293                {
294                    pmp->state = PMP_STATE_ADDING;
295                    tr_dbg( "nat-pmp state idle -> add with port %i",
296                            pmp->newport );
297                }
298                break;
299
300            case PMP_STATE_ADDING:
301                if( NULL == pmp->req )
302                {
303                    if( 0 >= pmp->newport )
304                    {
305                        tr_dbg( "nat-pmp state add -> idle, no port" );
306                        pmp->state = PMP_STATE_IDLE;
307                    }
308                    else if( INADDR_NONE == pmp->dest.s_addr )
309                    {
310                        tr_dbg( "nat-pmp state add -> fail, no default route" );
311                        pmp->state = PMP_STATE_FAILED;
312                    }
313                    else
314                    {
315                        pmp->req = newreq( 1, pmp->dest, pmp->newport,
316                                           pmp->fdlimit, &pmp->uptime );
317                        if( NULL == pmp->req )
318                        {
319                            pmp->state = PMP_STATE_FAILED;
320                            tr_dbg( "nat-pmp state add -> fail on req init" );
321                        }
322                    }
323                }
324                if( PMP_STATE_ADDING == pmp->state )
325                {
326                    switch( pulsereq( pmp->req, &pmp->renew ) )
327                    {
328                        case TR_ERROR:
329                            if( pmp->req->nobodyhome )
330                            {
331                                pmp->state = PMP_STATE_NOBODYHOME;
332                                tr_dbg( "nat-pmp state add -> nobodyhome on pulse" );
333                            }
334                            else
335                            {
336                                pmp->state = PMP_STATE_FAILED;
337                                tr_dbg( "nat-pmp state add -> fail on pulse" );
338                            }
339                            killreq( &pmp->req );
340                            break;
341                        case TR_OK:
342                            pmp->mappedport = pmp->req->port;
343                            killreq( &pmp->req );
344                            pmp->state = PMP_STATE_MAPPED;
345                            tr_dbg( "nat-pmp state add -> mapped with port %i",
346                                    pmp->mappedport);
347                            tr_inf( "nat-pmp mapped port %i", pmp->mappedport );
348                            break;
349                        case TR_WAIT:
350                            break;
351                    }
352                }
353                break;
354
355            case PMP_STATE_DELETING:
356                if( NULL == pmp->req )
357                {
358                    assert( 0 < pmp->mappedport );
359                    pmp->req = newreq( 0, pmp->dest, pmp->newport,
360                                       pmp->fdlimit, &pmp->uptime );
361                    if( NULL == pmp->req )
362                    {
363                        pmp->state = PMP_STATE_FAILED;
364                        tr_dbg( "nat-pmp state del -> fail on req init" );
365                    }
366                }
367                if( PMP_STATE_DELETING == pmp->state )
368                {
369                    switch( pulsereq( pmp->req, &pmp->renew ) )
370                    {
371                        case TR_ERROR:
372                            if( pmp->req->nobodyhome )
373                            {
374                                pmp->state = PMP_STATE_NOBODYHOME;
375                                tr_dbg( "nat-pmp state del -> nobodyhome on pulse" );
376                            }
377                            else
378                            {
379                                pmp->state = PMP_STATE_FAILED;
380                                tr_dbg( "nat-pmp state del -> fail on pulse" );
381                            }
382                            killreq( &pmp->req );
383                            break;
384                        case TR_OK:
385                            tr_dbg( "nat-pmp state del -> idle with port %i",
386                                    pmp->req->port);
387                            tr_inf( "nat-pmp unmapped port %i", pmp->req->port );
388                            pmp->mappedport = -1;
389                            killreq( &pmp->req );
390                            pmp->state = PMP_STATE_IDLE;
391                            break;
392                        case TR_WAIT:
393                            break;
394                    }
395                }
396                break;
397
398            case PMP_STATE_MAPPED:
399                if( pmp->newport != pmp->mappedport )
400                {
401                    tr_dbg( "nat-pmp state mapped -> del, port from %i to %i",
402                            pmp->mappedport, pmp->newport );
403                    pmp->state = PMP_STATE_DELETING;
404                }
405                else if( tr_date() > pmp->renew )
406                {
407                    pmp->state = PMP_STATE_ADDING;
408                    tr_dbg( "nat-pmp state mapped -> add for renewal" );
409                }
410                break;
411
412            case PMP_STATE_FAILED:
413            case PMP_STATE_NOBODYHOME:
414                break;
415
416            default:
417                assert( 0 );
418                break;
419        }
420    }
421
422    tr_lockUnlock( &pmp->lock );
423}
424
425static int
426checktime( tr_natpmp_uptime_t * uptime, uint32_t cursecs )
427{
428    time_t   now;
429    int      ret;
430    uint32_t estimated;
431
432    now = time( NULL );
433    ret = 0;
434    if( 0 < uptime->when )
435    {
436        estimated = ( ( now - uptime->when ) * 7 / 8 ) + uptime->uptime;
437        if( estimated > cursecs )
438        {
439            ret = 1;
440        }
441    }
442
443    uptime->when   = now;
444    uptime->uptime = cursecs;
445
446    return ret;
447}
448
449static void
450killsock( int * fd, tr_fd_t * fdlimit )
451{
452    if( 0 <= *fd )
453    {
454        tr_netClose( *fd );
455        *fd = -1;
456        tr_fdSocketClosed( fdlimit, 0 );
457    }
458}
459
460static tr_natpmp_req_t *
461newreq( int adding, struct in_addr addr, int port, tr_fd_t * fdlimit,
462        tr_natpmp_uptime_t * uptime )
463{
464    tr_natpmp_req_t * ret;
465    uint64_t          now;
466
467    ret = calloc( 1, sizeof( *ret ) );
468    if( NULL == ret )
469    {
470        goto err;
471    }
472    ret->fd = -1;
473    if( tr_fdSocketWillCreate( fdlimit, 0 ) )
474    {
475        goto err;
476    }
477    ret->fd = tr_netOpenUDP( addr, htons( PMP_PORT ) );
478    if( 0 > ret->fd )
479    {
480        goto err;
481    }
482    if( sendrequest( adding, ret->fd, port ) )
483    {
484        goto err;
485    }
486
487    now          = tr_date();
488    ret->adding  = adding;
489    ret->delay   = PMP_INITIAL_DELAY;
490    ret->retry   = now + PMP_INITIAL_DELAY;
491    ret->timeout = now + PMP_TOTAL_DELAY;
492    ret->port    = port;
493    ret->fdlimit = fdlimit;
494    ret->uptime  = uptime;
495
496    return ret;
497
498  err:
499    if( NULL != ret )
500    {
501        killsock( &ret->fd, fdlimit );
502    }
503    free( ret );
504
505    return NULL;
506}
507
508static tr_tristate_t
509pulsereq( tr_natpmp_req_t * req, uint64_t * renew )
510{
511    struct sockaddr_in sin;
512    uint8_t            buf[16];
513    int                res;
514    uint64_t           now;
515
516    now = tr_date();
517
518    if( now >= req->timeout )
519    {
520        tr_dbg( "nat-pmp request timed out" );
521        req->nobodyhome = 1;
522        return TR_ERROR;
523    }
524
525    if( now >= req->retry )
526    {
527        if( sendrequest( req->adding, req->fd, req->port ) )
528        {
529            return TR_ERROR;
530        }
531        req->delay *= 2;
532        req->timeout = now + req->delay;
533    }
534
535    res = tr_netRecvFrom( req->fd, buf, sizeof( buf ), &sin );
536    if( TR_NET_BLOCK & res )
537    {
538        return TR_WAIT;
539    }
540    else if( TR_NET_CLOSE & res )
541    {
542        if( ECONNRESET == errno || ECONNREFUSED == errno )
543        {
544            tr_dbg( "nat-pmp not supported by device" );
545            req->nobodyhome = 1;
546        }
547        else
548        {
549            tr_inf( "error reading nat-pmp response (%s)", strerror( errno ) );
550        }
551        return TR_ERROR;
552    }
553
554    tr_dbg( "nat-pmp read %i byte response", res );
555
556    return readrequest( buf, res, req->adding, req->port, req->uptime, renew );
557}
558
559static int
560mcastsetup( tr_fd_t * fdlimit )
561{
562    int fd;
563    struct in_addr addr;
564
565    if( tr_fdSocketWillCreate( fdlimit, 0 ) )
566    {
567        return -1;
568    }
569
570    addr.s_addr = inet_addr( PMP_MCAST_ADDR );
571    fd = tr_netMcastOpen( PMP_PORT, addr );
572    if( 0 > fd )
573    {
574        tr_fdSocketClosed( fdlimit, 0 );
575        return -1;
576    }
577
578    tr_dbg( "nat-pmp create multicast socket %i", fd );
579
580    return fd;
581}
582
583static void
584mcastpulse( tr_natpmp_t * pmp )
585{
586    struct sockaddr_in sin;
587    uint8_t            buf[16];
588    int                res;
589    char               dbgstr[INET_ADDRSTRLEN];
590
591    res = tr_netRecvFrom( pmp->mcastfd, buf, sizeof( buf ), &sin );
592    if( TR_NET_BLOCK & res )
593    {
594        return;
595    }
596    else if( TR_NET_CLOSE & res )
597    {
598        tr_err( "error reading nat-pmp multicast message" );
599        killsock( &pmp->mcastfd, pmp->fdlimit );
600        return;
601    }
602
603    tr_netNtop( &sin.sin_addr, dbgstr, sizeof( dbgstr ) );
604    tr_dbg( "nat-pmp read %i byte multicast packet from %s", res, dbgstr );
605
606    if( pmp->dest.s_addr != sin.sin_addr.s_addr )
607    {
608        tr_dbg( "nat-pmp ignoring multicast packet from unknown host %s",
609                dbgstr );
610        return;
611    }
612
613    if( TR_OK == readrequest( buf, res, 0, -1, &pmp->uptime, &pmp->renew ) &&
614        PMP_STATE_FAILED == pmp->state )
615    {
616        tr_dbg( "nat-pmp state fail -> idle" );
617        pmp->state = PMP_STATE_IDLE;
618    }
619}
620
621static void
622killreq( tr_natpmp_req_t ** req )
623{
624    if( NULL != *req )
625    {
626        killsock( &(*req)->fd, (*req)->fdlimit );
627        free( *req );
628        *req = NULL;
629    }
630}
631
632static int
633sendrequest( int adding, int fd, int port )
634{
635    uint8_t buf[12];
636    int res;
637
638    buf[0] = PMP_VERSION;
639    buf[1] = PMP_OPCODE_ADDTCP;
640    buf[2] = 0;
641    buf[3] = 0;
642    PMP_TOBUF16( buf + 4, port );
643    if( adding )
644    {
645        PMP_TOBUF16( buf + 6, port );
646        PMP_TOBUF32( buf + 8, PMP_LIFETIME );
647    }
648    else
649    {
650        PMP_TOBUF16( buf + 6, 0 );
651        PMP_TOBUF32( buf + 8, 0 );
652    }
653
654    res = tr_netSend( fd, buf, sizeof( buf ) );
655    /* XXX is it all right to assume the entire thing is written? */
656
657    /* XXX I should handle blocking here */
658
659    return ( ( TR_NET_CLOSE | TR_NET_BLOCK ) & res  ? 1 : 0 );
660}
661
662static tr_tristate_t
663readrequest( uint8_t * buf, int len, int adding, int port,
664             tr_natpmp_uptime_t * uptime, uint64_t * renew )
665{
666    uint8_t            version, opcode, wantedopcode;
667    uint16_t           rescode, privport, pubport;
668    uint32_t           seconds, lifetime;
669
670    if( 4 > len )
671    {
672        tr_err( "read truncated %i byte nat-pmp response packet", len );
673        return TR_ERROR;
674    }
675    version      = buf[0];
676    opcode       = buf[1];
677    rescode      = PMP_FROMBUF16( buf + 2 );
678    wantedopcode = ( 0 < port ? PMP_OPCODE_ADDTCP : PMP_OPCODE_GETIP );
679
680    if( !PMP_OPCODE_IS_RESPONSE( opcode ) )
681    {
682        tr_dbg( "nat-pmp ignoring request packet" );
683        return TR_WAIT;
684    }
685    opcode = PMP_OPCODE_FROM_RESPONSE( opcode );
686
687    if( PMP_VERSION != version )
688    {
689        tr_err( "bad nat-pmp version %hhu", buf[0] );
690        return TR_ERROR;
691    }
692    if( wantedopcode != opcode )
693    {
694        tr_err( "bad nat-pmp opcode %hhu", opcode );
695        return TR_ERROR;
696    }
697    if( PMP_RESPCODE_OK != rescode )
698    {
699        tr_err( "bad nat-pmp result code %hu", rescode );
700        return TR_ERROR;
701    }
702
703    if( 8 > len )
704    {
705        tr_err( "read truncated %i byte nat-pmp response packet", len );
706        return TR_ERROR;
707    }
708    seconds = PMP_FROMBUF32( buf + 4 );
709
710    if( checktime( uptime, seconds ) )
711    {
712        *renew = 0;
713        tr_inf( "detected nat-pmp device reset" );
714        /* XXX should reset retry counter here */
715        return TR_WAIT;
716    }
717
718    if( 0 <= port )
719    {
720        assert( PMP_OPCODE_ADDTCP == wantedopcode );
721        if( 16 > len )
722        {
723            tr_err( "read truncated %i byte nat-pmp response packet", len );
724            return TR_ERROR;
725        }
726        privport = PMP_FROMBUF16( buf + 8 );
727        pubport  = PMP_FROMBUF16( buf + 10 );
728        lifetime = PMP_FROMBUF32( buf + 12 );
729
730        if( port != privport )
731        {
732            /* private port doesn't match, ignore it */
733            tr_dbg( "nat-pmp ignoring message for port %i, expected port %i",
734                    privport, port );
735            return TR_WAIT;
736        }
737
738        if( adding )
739        {
740            if( port != pubport )
741            {
742                /* XXX should just start announcing the pub port we're given */
743                return TR_ERROR;
744            }
745            tr_dbg( "nat-pmp set renew to half of %u", lifetime );
746            *renew = tr_date() + ( lifetime / 2 * 1000 );
747        }
748    }
749
750    return TR_OK;
751}
Note: See TracBrowser for help on using the repository browser.