source: trunk/libtransmission/peerparse.h @ 2149

Last change on this file since 2149 was 2149, checked in by livings124, 15 years ago

Merge file selection and torrent creation into the main branch.

The new code for these features is under a new license.

  • Property svn:keywords set to Date Rev Author Id
File size: 21.0 KB
Line 
1/******************************************************************************
2 * $Id: peerparse.h 2149 2007-06-18 03:40:41Z livings124 $
3 *
4 * Copyright (c) 2005-2007 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/***********************************************************************
26 * This file handles all incoming messages
27 **********************************************************************/
28
29/***********************************************************************
30 * parseChoke
31 ***********************************************************************
32 *
33 **********************************************************************/
34static inline int parseChoke( tr_torrent_t * tor, tr_peer_t * peer,
35                              int len, int choking )
36{
37    tr_request_t * r;
38    int i;
39
40    if( len != 0 )
41    {
42        peer_dbg( "GET  %schoke, invalid", choking ? "" : "un" );
43        return TR_ERROR_ASSERT;
44    }
45
46    peer_dbg( "GET  %schoke", choking ? "" : "un" );
47
48    peer->peerChoking = choking;
49
50    if( choking )
51    {
52        /* Discard all pending requests */
53        for( i = 0; i < peer->inRequestCount; i++ )
54        {
55            r = &peer->inRequests[i];
56            tr_cpDownloaderRem( tor->completion, tr_block(r->index,r->begin) );
57
58            /* According to the spec, all requests are dropped when you
59               are choked, however some clients seem to remember those
60               the next time they unchoke you. Also, if you get quickly
61               choked and unchoked while you are sending requests, you
62               can't know when the other peer received them and how it
63               handled it.
64               This can cause us to receive blocks multiple times and
65               overdownload, so we send 'cancel' messages to try and
66               reduce that. */
67            sendCancel( peer, r->index, r->begin, r->length );
68        }
69        peer->inRequestCount = 0;
70    }
71
72    return TR_OK;
73}
74
75/***********************************************************************
76 * parseInterested
77 ***********************************************************************
78 *
79 **********************************************************************/
80static inline int parseInterested( tr_peer_t * peer, int len,
81                                   int interested )
82{
83    if( len != 0 )
84    {
85        peer_dbg( "GET  %sinterested, invalid", interested ? "" : "un" );
86        return TR_ERROR_ASSERT;
87    }
88
89    peer_dbg( "GET  %sinterested", interested ? "" : "un" );
90
91    peer->peerInterested = interested;
92
93    return TR_OK;
94}
95
96/***********************************************************************
97 * parseHave
98 ***********************************************************************
99 *
100 **********************************************************************/
101static inline int parseHave( tr_torrent_t * tor, tr_peer_t * peer,
102                             uint8_t * p, int len )
103{
104    tr_info_t * inf = &tor->info;
105    uint32_t piece;
106
107    if( len != 4 )
108    {
109        peer_dbg( "GET  have, invalid" );
110        return TR_ERROR_ASSERT;
111    }
112
113    TR_NTOHL( p, piece );
114    if( ( uint32_t )inf->pieceCount <= piece )
115    {
116        peer_dbg( "GET  have, invalid piece" );
117        return TR_ERROR_ASSERT;
118    }
119
120    peer_dbg( "GET  have %d", piece );
121
122    if( !peer->bitfield )
123    {
124        peer->bitfield = tr_bitfieldNew( inf->pieceCount );
125    }
126    if( !tr_bitfieldHas( peer->bitfield, piece ) )
127    {
128        peer->pieceCount++;
129        peer->progress = (float) peer->pieceCount / inf->pieceCount;
130    }
131    tr_bitfieldAdd( peer->bitfield, piece );
132    updateInterest( tor, peer );
133
134    tr_rcTransferred( tor->swarmspeed, tor->info.pieceSize );
135
136    return TR_OK;
137}
138
139static inline int parseBitfield( tr_torrent_t * tor, tr_peer_t * peer,
140                                 uint8_t * p, int len )
141{
142    tr_info_t * inf = &tor->info;
143    int bitfieldSize;
144    int i;
145
146    bitfieldSize = ( inf->pieceCount + 7 ) / 8;
147   
148    if( len != bitfieldSize )
149    {
150        peer_dbg( "GET  bitfield, wrong size" );
151        return TR_ERROR_ASSERT;
152    }
153
154    /* Make sure the spare bits are unset */
155    if( ( inf->pieceCount & 0x7 ) )
156    {
157        uint8_t lastByte;
158       
159        lastByte   = p[bitfieldSize-1];
160        lastByte <<= inf->pieceCount & 0x7;
161        lastByte  &= 0xFF;
162
163        if( lastByte )
164        {
165            peer_dbg( "GET  bitfield, spare bits set" );
166            return TR_ERROR_ASSERT;
167        }
168    }
169
170    peer_dbg( "GET  bitfield, ok" );
171
172    if( !peer->bitfield )
173    {
174        peer->bitfield = tr_bitfieldNew( inf->pieceCount );
175    }
176    assert( (unsigned)bitfieldSize == peer->bitfield->len );
177    memcpy( peer->bitfield->bits, p, bitfieldSize );
178
179    peer->pieceCount = 0;
180    for( i = 0; i < inf->pieceCount; i++ )
181    {
182        if( tr_bitfieldHas( peer->bitfield, i ) )
183        {
184            peer->pieceCount++;
185        }
186    }
187    peer->progress = (float) peer->pieceCount / inf->pieceCount;
188
189    updateInterest( tor, peer );
190
191    return TR_OK;
192}
193
194static inline int parseRequest( tr_torrent_t * tor, tr_peer_t * peer,
195                                uint8_t * p, int len )
196{
197    tr_info_t * inf = &tor->info;
198    int index, begin, length;
199    tr_request_t * r;
200
201    if( len != 12 )
202    {
203        peer_dbg( "GET  request, invalid" );
204        return TR_ERROR_ASSERT;
205    }
206
207    if( peer->amChoking )
208    {
209        /* Didn't he get it? */
210        sendChoke( peer, 1 );
211        return TR_OK;
212    }
213   
214    TR_NTOHL( p,     index );
215    TR_NTOHL( &p[4], begin );
216    TR_NTOHL( &p[8], length );
217
218    if( inf->pieceCount <= index )
219    {
220        peer_dbg( "GET  request, invalid index" );
221        return TR_ERROR_ASSERT;
222    }
223    if( tr_pieceSize( index ) < begin + length )
224    {
225        peer_dbg( "GET  request, invalid begin/length" );
226        return TR_ERROR_ASSERT;
227    }
228
229    peer_dbg( "GET  request %d/%d (%d bytes)",
230              index, begin, length );
231
232    /* TODO sanity checks (do we have the piece, etc) */
233
234    if( length > 16384 )
235    {
236        /* Sorry mate */
237        return TR_ERROR;
238    }
239
240    if( peer->outRequestCount >= MAX_REQUEST_COUNT )
241    {
242        tr_err( "Too many requests" );
243        return TR_ERROR;
244    }
245
246    r         = &peer->outRequests[peer->outRequestCount];
247    r->index  = index;
248    r->begin  = begin;
249    r->length = length;
250
251    (peer->outRequestCount)++;
252
253    return TR_OK;
254}
255
256static inline void updateRequests( tr_peer_t * peer, int index, int begin )
257{
258    tr_request_t * r;
259    int i;
260
261    /* Find this block in the requests list */
262    for( i = 0; i < peer->inRequestCount; i++ )
263    {
264        r = &peer->inRequests[i];
265        if( index == r->index && begin == r->begin )
266        {
267            break;
268        }
269    }
270
271    /* Usually 'i' would be 0, but some clients don't handle multiple
272       requests and drop previous requests, some other clients don't
273       send blocks in the same order we sent the requests */
274    if( i < peer->inRequestCount )
275    {
276        peer->inRequestCount--;
277        memmove( &peer->inRequests[i], &peer->inRequests[i+1],
278                 ( peer->inRequestCount - i ) * sizeof( tr_request_t ) );
279    }
280    else
281    {
282        /* Not in the list. Probably because of a cancel that arrived
283           too late */
284        peer_dbg( "wasn't expecting this block" );
285    }
286}
287
288static inline int parsePiece( tr_torrent_t * tor, tr_peer_t * peer,
289                              uint8_t * p, int len )
290{
291    tr_info_t * inf = &tor->info;
292    int index, begin, block, i, ret;
293
294    if( 8 > len )
295    {
296        peer_dbg( "GET  piece, too short (8 > %i)", len );
297        return TR_ERROR_ASSERT;
298    }
299
300    TR_NTOHL( p,     index );
301    TR_NTOHL( &p[4], begin );
302
303    if( inf->pieceCount <= index )
304    {
305        peer_dbg( "GET  piece, invalid index" );
306        return TR_ERROR_ASSERT;
307    }
308    if( tr_pieceSize( index ) < begin + len - 8 )
309    {
310        peer_dbg( "GET  piece, invalid begin/length" );
311        return TR_ERROR_ASSERT;
312    }
313
314    block = tr_block( index, begin );
315
316    peer_dbg( "GET  piece %d/%d (%d bytes)",
317              index, begin, len - 8 );
318
319    updateRequests( peer, index, begin );
320    tor->downloadedCur += len - 8;
321
322    /* Sanity checks */
323    if( len - 8 != tr_blockSize( block ) )
324    {
325        peer_dbg( "wrong size (expecting %d)", tr_blockSize( block ) );
326        return TR_ERROR_ASSERT;
327    }
328    if( tr_cpBlockIsComplete( tor->completion, block ) )
329    {
330        peer_dbg( "have this block already" );
331        return TR_OK;
332    }
333
334    /* Set blame/credit for this piece */
335    if( !peer->blamefield )
336    {
337        peer->blamefield = tr_bitfieldNew( inf->pieceCount );
338    }
339    tr_bitfieldAdd( peer->blamefield, index );
340
341    /* Write to disk */
342    if( ( ret = tr_ioWrite( tor->io, index, begin, len - 8, &p[8] ) ) )
343    {
344        return ret;
345    }
346    tr_cpBlockAdd( tor->completion, block );
347    broadcastCancel( tor, index, begin, len - 8 );
348
349    if( !tr_cpPieceHasAllBlocks( tor->completion, index ) )
350    {
351        return TR_OK;
352    }
353
354    /* Piece is complete, check it */
355    if( ( ret = tr_ioHash( tor->io, index ) ) )
356    {
357        return ret;
358    }
359    if( !tr_cpPieceIsComplete( tor->completion, index ) )
360    {
361        return TR_OK;
362    }
363
364    /* Hash OK */
365    for( i = 0; i < tor->peerCount; i++ )
366    {
367        tr_peer_t * otherPeer;
368        otherPeer = tor->peers[i];
369
370        if( otherPeer->status < PEER_STATUS_CONNECTED )
371            continue;
372
373        sendHave( otherPeer, index );
374        updateInterest( tor, otherPeer );
375    }
376
377    return TR_OK;
378}
379
380static inline int parseCancel( tr_torrent_t * tor, tr_peer_t * peer,
381                               uint8_t * p, int len )
382{
383    tr_info_t * inf = &tor->info;
384    int index, begin, length;
385    int i;
386    tr_request_t * r;
387
388    if( len != 12 )
389    {
390        peer_dbg( "GET  cancel, invalid" );
391        return TR_ERROR_ASSERT;
392    }
393
394    TR_NTOHL( p,     index );
395    TR_NTOHL( &p[4], begin );
396    TR_NTOHL( &p[8], length );
397
398    if( inf->pieceCount <= index )
399    {
400        peer_dbg( "GET  cancel, invalid index" );
401        return TR_ERROR_ASSERT;
402    }
403    if( tr_pieceSize( index ) < begin + length )
404    {
405        peer_dbg( "GET  cancel, invalid begin/length" );
406        return TR_ERROR_ASSERT;
407    }
408
409    peer_dbg( "GET  cancel %d/%d (%d bytes)",
410              index, begin, length );
411
412    for( i = 0; i < peer->outRequestCount; i++ )
413    {
414        r = &peer->outRequests[i];
415        if( r->index == index && r->begin == begin &&
416            r->length == length )
417        {
418            (peer->outRequestCount)--;
419            memmove( &r[0], &r[1], sizeof( tr_request_t ) *
420                    ( peer->outRequestCount - i ) );
421            break;
422        }
423    }
424
425    return TR_OK;
426}
427
428static inline int parsePort( tr_peer_t * peer, uint8_t * p, int len )
429{
430    in_port_t port;
431
432    if( len != 2 )
433    {
434        peer_dbg( "GET  port, invalid" );
435        return TR_ERROR_ASSERT;
436    }
437
438    port = *( (in_port_t *) p );
439    peer_dbg( "GET  port %d", ntohs( port ) );
440
441    return TR_OK;
442}
443
444static inline int
445parseMessageHeader( tr_peer_t * peer, uint8_t * buf, int buflen,
446                    int * msgid, int * msglen )
447{
448    if( 4 > buflen )
449    {
450        return TR_NET_BLOCK;
451    }
452
453    /* Get payload size */
454    TR_NTOHL( buf, *msglen );
455
456    if( 4 + *msglen > buflen )
457    {
458        /* We do not have the entire message */
459        return TR_NET_BLOCK;
460    }
461
462    if( 0 == *msglen )
463    {
464        /* keep-alive */
465        peer_dbg( "GET  keep-alive" );
466        *msgid = AZ_MSG_BT_KEEP_ALIVE;
467        return 4;
468    }
469    else
470    {
471        /* Type of the message */
472        *msgid = buf[4];
473        (*msglen)--;
474        return 5;
475    }
476}
477
478static inline int parseMessage( tr_torrent_t * tor, tr_peer_t * peer,
479                                int id, uint8_t * p, int len )
480{
481    int extid;
482
483    switch( id )
484    {
485        case PEER_MSG_CHOKE:
486            return parseChoke( tor, peer, len, 1 );
487        case PEER_MSG_UNCHOKE:
488            return parseChoke( tor, peer, len, 0 );
489        case PEER_MSG_INTERESTED:
490            return parseInterested( peer, len, 1 );
491        case PEER_MSG_UNINTERESTED:
492            return parseInterested( peer, len, 0 );
493        case PEER_MSG_HAVE:
494            return parseHave( tor, peer, p, len );
495        case PEER_MSG_BITFIELD:
496            return parseBitfield( tor, peer, p, len );
497        case PEER_MSG_REQUEST:
498            return parseRequest( tor, peer, p, len );
499        case PEER_MSG_PIECE:
500            return parsePiece( tor, peer, p, len );
501        case PEER_MSG_CANCEL:
502            return parseCancel( tor, peer, p, len );
503        case PEER_MSG_PORT:
504            return parsePort( peer, p, len );
505        case PEER_MSG_EXTENDED:
506            if( EXTENDED_NOT_SUPPORTED == peer->extStatus )
507            {
508                break;
509            }
510            if( 0 < len )
511            {
512                extid = p[0];
513                p++;
514                len--;
515                if( EXTENDED_HANDSHAKE_ID == extid )
516                {
517                    return parseExtendedHandshake( peer, p, len );
518                }
519                else if( 0 < peer->pexStatus && extid == peer->pexStatus )
520                {
521                    return parseUTPex( tor, peer, p, len );
522                }
523                peer_dbg( "GET  unknown extended message '%hhu'", extid );
524            }
525            return 1;
526        case AZ_MSG_BT_KEEP_ALIVE:
527            return TR_OK;
528        case AZ_MSG_AZ_PEER_EXCHANGE:
529            if( peer->azproto && peer->pexStatus )
530            {
531                return parseAZPex( tor, peer, p, len );
532            }
533            break;
534        case AZ_MSG_INVALID:
535            return 0;
536    }
537
538    peer_dbg( "GET  unknown message '%d'", id );
539    return TR_ERROR;
540}
541
542static inline int parseBufHeader( tr_peer_t * peer )
543{
544    static uint8_t badproto_http[] =
545"HTTP/1.0 400 Nice try...\015\012"
546"Content-type: text/plain\015\012"
547"\015\012";
548    static uint8_t badproto_tinfoil[] =
549"This is not a rootkit or other backdoor, it's a BitTorrent\015\012"
550"client. Really. Why should you be worried, can't you read this\015\012"
551"reassuring message? Now just listen to this social engi, er, I mean,\015\012"
552"completely truthful statement, and go about your business. Your box is\015\012"
553"safe and completely impregnable, the marketing hype for your OS even\015\012"
554"says so. You can believe everything you read. Now move along, nothing\015\012"
555"to see here.";
556    uint8_t * p   = peer->buf;
557
558    if( 4 > peer->pos )
559    {
560        return TR_OK;
561    }
562
563    if( p[0] != 19 || memcmp( &p[1], "Bit", 3 ) )
564    {
565        /* Don't wait until we get 68 bytes, this is wrong
566           already */
567        peer_dbg( "GET  handshake, invalid" );
568        if( 0 == memcmp( p, "GET ", 4 ) || 0 == memcmp( p, "HEAD", 4 ) )
569        {
570            tr_netSend( peer->socket, badproto_http, sizeof badproto_http - 1 );
571        }
572        tr_netSend( peer->socket, badproto_tinfoil, sizeof badproto_tinfoil - 1 );
573        return TR_ERROR;
574    }
575    if( peer->pos < 68 )
576    {
577        return TR_OK;
578    }
579    if( memcmp( &p[4], "Torrent protocol", 16 ) )
580    {
581        peer_dbg( "GET  handshake, invalid" );
582        return TR_ERROR;
583    }
584
585    return TR_OK;
586}
587
588static uint8_t * parseBufHash( tr_peer_t * peer )
589{
590    if( 48 > peer->pos )
591    {
592        return NULL;
593    }
594    else
595    {
596        return peer->buf + 28;
597    }
598}
599
600static inline int parseHandshake( tr_torrent_t * tor, tr_peer_t * peer )
601{
602    tr_info_t * inf = &tor->info;
603    int         ii;
604
605    if( memcmp( &peer->buf[28], inf->hash, SHA_DIGEST_LENGTH ) )
606    {
607        peer_dbg( "GET  handshake, wrong torrent hash" );
608        return TR_ERROR;
609    }
610
611    if( !memcmp( &peer->buf[48], tor->id, TR_ID_LEN ) )
612    {
613        /* We are connected to ourselves... */
614        peer_dbg( "GET  handshake, that is us" );
615        return TR_ERROR;
616    }
617
618    memcpy( peer->id, &peer->buf[48], TR_ID_LEN );
619
620    for( ii = 0; ii < tor->peerCount; ii++ )
621    {
622        if( tor->peers[ii] == peer )
623        {
624            continue;
625        }
626        if( !peerCmp( peer, tor->peers[ii] ) )
627        {
628            peer_dbg( "GET  handshake, duplicate" );
629            return TR_ERROR;
630        }
631    }
632
633    if( PEER_SUPPORTS_EXTENDED_MESSAGES( &peer->buf[20] ) )
634    {
635        peer->status = PEER_STATUS_CONNECTED;
636        peer->extStatus = EXTENDED_SUPPORTED;
637        peer_dbg( "GET  handshake, ok (%s) extended messaging supported",
638                  tr_peerClient( peer ) );
639    }
640    else if( PEER_SUPPORTS_AZUREUS_PROTOCOL( &peer->buf[20] ) )
641    {
642        peer->status  = PEER_STATUS_AZ_GIVER;
643        peer->azproto = 1;
644        peer->date    = tr_date();
645        peer_dbg( "GET  handshake, ok (%s) will use azureus protocol",
646                  tr_peerClient( peer ) );
647    }
648    else
649    {
650        peer->status = PEER_STATUS_CONNECTED;
651        peer_dbg( "GET  handshake, ok (%s)", tr_peerClient( peer ) );
652    }
653
654    return TR_OK;
655}
656
657static inline int sendInitial( tr_torrent_t * tor, tr_peer_t * peer )
658{
659    if( PEER_STATUS_CONNECTED != peer->status )
660    {
661        return TR_OK;
662    }
663
664    if( EXTENDED_SUPPORTED == peer->extStatus )
665    {
666        if( sendExtended( tor, peer, EXTENDED_HANDSHAKE_ID ) )
667        {
668            return TR_ERROR;
669        }
670        peer->extStatus = EXTENDED_HANDSHAKE;
671    }
672
673    sendBitfield( tor, peer );
674
675    return TR_OK;
676}
677
678static inline int parseBuf( tr_torrent_t * tor, tr_peer_t * peer )
679{
680    int       len, ret, msgid;
681    uint8_t * buf;
682
683    buf = peer->buf;
684
685    if( peer->banned )
686    {
687        /* Don't even parse, we only stay connected */
688        peer->pos = 0;
689        return TR_OK;
690    }
691
692    while( peer->pos >= 4 )
693    {
694        if( PEER_STATUS_HANDSHAKE == peer->status )
695        {
696            ret = parseBufHeader( peer );
697            if( ret )
698            {
699                return ret;
700            }
701
702            if( peer->pos < 68 )
703            {
704                break;
705            }
706
707            ret = parseHandshake( tor, peer );
708            if( 0 > ret )
709            {
710                return ret;
711            }
712            buf       += 68;
713            peer->pos -= 68;
714
715            ret = sendInitial( tor, peer );
716            if( ret )
717            {
718                return ret;
719            }
720
721            continue;
722        }
723
724        if( PEER_STATUS_AZ_RECEIVER == peer->status )
725        {
726            ret = parseAZMessageHeader( peer, buf, peer->pos, &msgid, &len );
727            if( TR_NET_BLOCK & ret )
728            {
729                break;
730            }
731            else if( TR_NET_CLOSE & ret )
732            {
733                return TR_ERROR;
734            }
735
736            buf       += ret;
737            peer->pos -= ret;
738            assert( len <= peer->pos );
739            if( AZ_MSG_AZ_HANDSHAKE != msgid ||
740                parseAZHandshake( peer, buf, len ) )
741            {
742                return TR_ERROR;
743            }
744            buf         += len;
745            peer->pos   -= len;
746            assert( 0 <= peer->pos );
747            peer->status = PEER_STATUS_CONNECTED;
748
749            ret = sendInitial( tor, peer );
750            if( ret )
751            {
752                return ret;
753            }
754
755            continue;
756        }
757
758        if( PEER_STATUS_CONNECTED != peer->status )
759        {
760            break;
761        }
762
763        if( peer->azproto )
764        {
765            ret = parseAZMessageHeader( peer, buf, peer->pos, &msgid, &len );
766        }
767        else
768        {
769            ret = parseMessageHeader( peer, buf, peer->pos, &msgid, &len );
770        }
771        if( TR_NET_BLOCK & ret )
772        {
773            break;
774        }
775        else if( TR_NET_CLOSE & ret )
776        {
777            return TR_ERROR;
778        }
779
780#if 0
781        if( len > 8 + tor->blockSize )
782        {
783            /* This should never happen. Drop that peer */
784            /* XXX could an extended message be longer than this? */
785            peer_dbg( "message too large (%d bytes)", len );
786            return TR_ERROR;
787        }
788#endif
789
790        buf       += ret;
791        peer->pos -= ret;
792        assert( 0 <= peer->pos );
793
794        if( ( ret = parseMessage( tor, peer, msgid, buf, len ) ) )
795        {
796            return ret;
797        }
798
799        buf       += len;
800        peer->pos -= len;
801        assert( 0 <= peer->pos );
802    }
803
804    if( 0 < peer->pos )
805    {
806        memmove( peer->buf, buf, peer->pos );
807    }
808
809    return TR_OK;
810}
Note: See TracBrowser for help on using the repository browser.