source: trunk/libtransmission/metainfo.c @ 10084

Last change on this file since 10084 was 10084, checked in by charles, 12 years ago

(trunk) #2802, #2716, #2717 -- remember magnet links and their settings between sessions, and allow their trackers to be modified

  • Property svn:keywords set to Date Rev Author Id
File size: 17.7 KB
Line 
1/*
2 * This file Copyright (C) 2009-2010 Mnemosyne LLC
3            trackers[trackerCount].id = 0;
4 *
5 * This file is licensed by the GPL version 2.  Works owned by the
6 * Transmission project are granted a special exemption to clause 2(b)
7 * so that the bulk of its code can remain under the MIT license.
8 * This exemption does not extend to derived works not owned by
9 * the Transmission project.
10 *
11 * $Id: metainfo.c 10084 2010-02-02 22:45:22Z charles $
12 */
13
14#include <assert.h>
15#include <errno.h>
16#include <stdio.h>
17#include <string.h>
18
19#include <sys/types.h>
20#include <sys/stat.h>
21#include <unistd.h> /* unlink, stat */
22
23#include <event.h> /* struct evbuffer */
24
25#include "transmission.h"
26#include "session.h"
27#include "bencode.h"
28#include "crypto.h" /* tr_sha1 */
29#include "metainfo.h"
30#include "platform.h"
31#include "utils.h"
32
33/***
34****
35***/
36
37static char*
38getTorrentFilename( const tr_session * session,
39                    const tr_info *   inf )
40{
41    return tr_strdup_printf( "%s%c%s.%16.16s.torrent",
42                             tr_getTorrentDir( session ),
43                             TR_PATH_DELIMITER,
44                             inf->name,
45                             inf->hashString );
46}
47
48static char*
49getOldTorrentFilename( const tr_session * session, const tr_info * inf )
50{
51    int i;
52    char * path;
53    struct stat sb;
54    const int tagCount = 5;
55    const char * tags[] = { "beos", "cli", "daemon", "macosx", "wx" };
56
57    /* test the beos, cli, daemon, macosx, wx tags */
58    for( i=0; i<tagCount; ++i ) {
59        path = tr_strdup_printf( "%s%c%s-%s", tr_getTorrentDir( session ), '/', inf->hashString, tags[i] );
60        if( !stat( path, &sb ) && ( ( sb.st_mode & S_IFMT ) == S_IFREG ) )
61            return path;
62        tr_free( path );
63    }
64
65    /* test a non-tagged file */
66    path = tr_buildPath( tr_getTorrentDir( session ), inf->hashString, NULL );
67    if( !stat( path, &sb ) && ( ( sb.st_mode & S_IFMT ) == S_IFREG ) )
68        return path;
69    tr_free( path );
70
71    /* return the -gtk form by default, since that's the most common case.
72       don't bother testing stat() on it since this is the last candidate
73       and we don't want to return NULL anyway */
74    return tr_strdup_printf( "%s%c%s-%s", tr_getTorrentDir( session ), '/', inf->hashString, "gtk" );
75}
76
77/* this is for really old versions of T and will probably be removed someday */
78void
79tr_metainfoMigrate( tr_session * session,
80                    tr_info *   inf )
81{
82    struct stat new_sb;
83    char *      name = getTorrentFilename( session, inf );
84
85    if( stat( name, &new_sb ) || ( ( new_sb.st_mode & S_IFMT ) != S_IFREG ) )
86    {
87        char *    old_name = getOldTorrentFilename( session, inf );
88        size_t    contentLen;
89        uint8_t * content;
90
91        tr_mkdirp( tr_getTorrentDir( session ), 0777 );
92        if( ( content = tr_loadFile( old_name, &contentLen ) ) )
93        {
94            FILE * out;
95            errno = 0;
96            out = fopen( name, "wb+" );
97            if( !out )
98            {
99                tr_nerr( inf->name, _( "Couldn't create \"%1$s\": %2$s" ),
100                        name, tr_strerror( errno ) );
101            }
102            else
103            {
104                if( fwrite( content, sizeof( uint8_t ), contentLen, out )
105                    == contentLen )
106                {
107                    tr_free( inf->torrent );
108                    inf->torrent = tr_strdup( name );
109                    tr_sessionSetTorrentFile( session, inf->hashString, name );
110                    unlink( old_name );
111                }
112                fclose( out );
113            }
114        }
115
116        tr_free( content );
117        tr_free( old_name );
118    }
119
120    tr_free( name );
121}
122
123/***
124****
125***/
126
127static tr_bool
128path_is_suspicious( const char * path )
129{
130    return ( path == NULL )
131        || ( strstr( path, "../" ) != NULL );
132}
133
134static tr_bool
135getfile( char ** setme, const char * root, tr_benc * path )
136{
137    tr_bool success = FALSE;
138
139    if( tr_bencIsList( path ) )
140    {
141        int i;
142        const int n = tr_bencListSize( path );
143        struct evbuffer * buf = evbuffer_new( );
144
145        evbuffer_add( buf, root, strlen( root ) );
146        for( i = 0; i < n; ++i )
147        {
148            const char * str;
149            if( tr_bencGetStr( tr_bencListChild( path, i ), &str ) )
150            {
151                evbuffer_add( buf, TR_PATH_DELIMITER_STR, 1 );
152                evbuffer_add( buf, str, strlen( str ) );
153            }
154        }
155
156        *setme = tr_utf8clean( (char*)EVBUFFER_DATA( buf ), EVBUFFER_LENGTH( buf ), NULL );
157        /* fprintf( stderr, "[%s]\n", *setme ); */
158        evbuffer_free( buf );
159        success = TRUE;
160    }
161
162    if( ( *setme != NULL ) && path_is_suspicious( *setme ) )
163    {
164        tr_free( *setme );
165        *setme = NULL;
166        success = FALSE;
167    }
168
169    return success;
170}
171
172static const char*
173parseFiles( tr_info * inf, tr_benc * files, const tr_benc * length )
174{
175    int64_t len;
176
177    inf->totalSize = 0;
178
179    if( tr_bencIsList( files ) ) /* multi-file mode */
180    {
181        tr_file_index_t i;
182
183        inf->isMultifile = 1;
184        inf->fileCount   = tr_bencListSize( files );
185        inf->files       = tr_new0( tr_file, inf->fileCount );
186
187        for( i = 0; i < inf->fileCount; ++i )
188        {
189            tr_benc * file;
190            tr_benc * path;
191
192            file = tr_bencListChild( files, i );
193            if( !tr_bencIsDict( file ) )
194                return "files";
195
196            if( !tr_bencDictFindList( file, "path.utf-8", &path ) )
197                if( !tr_bencDictFindList( file, "path", &path ) )
198                    return "path";
199
200            if( !getfile( &inf->files[i].name, inf->name, path ) )
201                return "path";
202
203            if( !tr_bencDictFindInt( file, "length", &len ) )
204                return "length";
205
206            inf->files[i].length = len;
207            inf->totalSize      += len;
208        }
209    }
210    else if( tr_bencGetInt( length, &len ) ) /* single-file mode */
211    {
212        if( path_is_suspicious( inf->name ) )
213            return "path";
214
215        inf->isMultifile      = 0;
216        inf->fileCount        = 1;
217        inf->files            = tr_new0( tr_file, 1 );
218        inf->files[0].name    = tr_strdup( inf->name );
219        inf->files[0].length  = len;
220        inf->totalSize       += len;
221    }
222    else
223    {
224        return "length";
225    }
226
227    return NULL;
228}
229
230static char *
231tr_convertAnnounceToScrape( const char * announce )
232{
233    char *       scrape = NULL;
234    const char * s;
235
236    /* To derive the scrape URL use the following steps:
237     * Begin with the announce URL. Find the last '/' in it.
238     * If the text immediately following that '/' isn't 'announce'
239     * it will be taken as a sign that that tracker doesn't support
240     * the scrape convention. If it does, substitute 'scrape' for
241     * 'announce' to find the scrape page.  */
242    if( ( ( s = strrchr( announce, '/' ) ) ) && !strncmp( ++s, "announce", 8 ) )
243    {
244        const char * prefix = announce;
245        const size_t prefix_len = s - announce;
246        const char * suffix = s + 8;
247        const size_t suffix_len = strlen( suffix );
248        const size_t alloc_len = prefix_len + 6 + suffix_len + 1;
249        char * walk = scrape = tr_new( char, alloc_len );
250        memcpy( walk, prefix, prefix_len ); walk += prefix_len;
251        memcpy( walk, "scrape", 6 );        walk += 6;
252        memcpy( walk, suffix, suffix_len ); walk += suffix_len;
253        *walk++ = '\0';
254        assert( walk - scrape == (int)alloc_len );
255    }
256
257    return scrape;
258}
259
260static const char*
261getannounce( tr_info * inf, tr_benc * meta )
262{
263    const char *      str;
264    tr_tracker_info * trackers = NULL;
265    int               trackerCount = 0;
266    tr_benc *         tiers;
267
268    /* Announce-list */
269    if( tr_bencDictFindList( meta, "announce-list", &tiers ) )
270    {
271        int       n;
272        int       i, j, validTiers;
273        const int numTiers = tr_bencListSize( tiers );
274
275        n = 0;
276        for( i = 0; i < numTiers; ++i )
277            n += tr_bencListSize( tr_bencListChild( tiers, i ) );
278
279        trackers = tr_new0( tr_tracker_info, n );
280
281        for( i = 0, validTiers = 0; i < numTiers; ++i )
282        {
283            tr_benc * tier = tr_bencListChild( tiers, i );
284            const int tierSize = tr_bencListSize( tier );
285            tr_bool anyAdded = FALSE;
286            for( j = 0; j < tierSize; ++j )
287            {
288                if( tr_bencGetStr( tr_bencListChild( tier, j ), &str ) )
289                {
290                    char * url = tr_strstrip( tr_strdup( str ) );
291                    if( tr_httpIsValidURL( url ) )
292                    {
293                        tr_tracker_info * t = trackers + trackerCount;
294                        t->tier = validTiers;
295                        t->announce = tr_strdup( url );
296                        t->scrape = tr_convertAnnounceToScrape( url );
297                        t->id = trackerCount;
298
299                        anyAdded = TRUE;
300                        ++trackerCount;
301                    }
302                    tr_free( url );
303                }
304            }
305
306            if( anyAdded )
307                ++validTiers;
308        }
309
310        /* did we use any of the tiers? */
311        if( !trackerCount )
312        {
313            tr_free( trackers );
314            trackers = NULL;
315        }
316    }
317
318    /* Regular announce value */
319    if( !trackerCount
320      && tr_bencDictFindStr( meta, "announce", &str ) )
321    {
322        char * url = tr_strstrip( tr_strdup( str ) );
323        if( tr_httpIsValidURL( url ) )
324        {
325            trackers = tr_new0( tr_tracker_info, 1 );
326            trackers[trackerCount].tier = 0;
327            trackers[trackerCount].announce = tr_strdup( url );
328            trackers[trackerCount].scrape = tr_convertAnnounceToScrape( url );
329            trackers[trackerCount].id = 0;
330            trackerCount++;
331            /*fprintf( stderr, "single announce: [%s]\n", url );*/
332        }
333        tr_free( url );
334    }
335
336    inf->trackers = trackers;
337    inf->trackerCount = trackerCount;
338
339    return NULL;
340}
341
342static void
343geturllist( tr_info * inf,
344            tr_benc * meta )
345{
346    tr_benc * urls;
347
348    if( tr_bencDictFindList( meta, "url-list", &urls ) )
349    {
350        int          i;
351        const char * url;
352        const int    n = tr_bencListSize( urls );
353
354        inf->webseedCount = 0;
355        inf->webseeds = tr_new0( char*, n );
356
357        for( i = 0; i < n; ++i )
358            if( tr_bencGetStr( tr_bencListChild( urls, i ), &url ) )
359                inf->webseeds[inf->webseedCount++] = tr_strdup( url );
360    }
361}
362
363static int
364is_rfc2396_alnum( char ch )
365{
366    return ( '0' <= ch && ch <= '9' )
367        || ( 'A' <= ch && ch <= 'Z' )
368        || ( 'a' <= ch && ch <= 'z' );
369}
370
371static void
372escape( char * out, const uint8_t * in, size_t in_len ) /* rfc2396 */
373{
374    const uint8_t *end = in + in_len;
375
376    while( in != end )
377        if( is_rfc2396_alnum( *in ) )
378            *out++ = (char) *in++;
379        else
380            out += tr_snprintf( out, 4, "%%%02X", (unsigned int)*in++ );
381
382    *out = '\0';
383}
384
385static const char*
386tr_metainfoParseImpl( const tr_session  * session,
387                      tr_info           * inf,
388                      tr_bool           * hasInfoDict,
389                      int               * infoDictOffset,
390                      int               * infoDictLength,
391                      const tr_benc     * meta_in )
392{
393    int64_t         i;
394    size_t          raw_len;
395    const char *    str;
396    const uint8_t * raw;
397    tr_benc *       d;
398    tr_benc *       infoDict = NULL;
399    tr_benc *       meta = (tr_benc *) meta_in;
400    tr_bool         err;
401    tr_bool         b;
402    tr_bool         isMagnet = FALSE;
403
404    /* info_hash: urlencoded 20-byte SHA1 hash of the value of the info key
405     * from the Metainfo file. Note that the value will be a bencoded
406     * dictionary, given the definition of the info key above. */
407    b = tr_bencDictFindDict( meta, "info", &infoDict );
408    if( hasInfoDict != NULL )
409        *hasInfoDict = b;
410    if( !b )
411    {
412        /* no info dictionary... is this a magnet link? */
413        if( tr_bencDictFindDict( meta, "magnet-info", &d ) )
414        {
415            isMagnet = TRUE;
416
417            /* get the info-hash */
418            if( !tr_bencDictFindRaw( d, "info_hash", &raw, &raw_len ) )
419                return "info_hash";
420            if( raw_len != SHA_DIGEST_LENGTH )
421                return "info_hash";
422            memcpy( inf->hash, raw, raw_len );
423            tr_sha1_to_hex( inf->hashString, inf->hash );
424            escape( inf->hashEscaped, inf->hash, SHA_DIGEST_LENGTH );
425
426            /* maybe get the display name */
427            if( tr_bencDictFindStr( d, "display-name", &str ) ) {
428                tr_free( inf->name );
429                inf->name = tr_strdup( str );
430            }
431        }
432        else /* not a magnet link and has no info dict... */
433        {
434            return "info";
435        }
436    }
437    else
438    {
439        int len;
440        char * bstr = tr_bencToStr( infoDict, TR_FMT_BENC, &len );
441        tr_sha1( inf->hash, bstr, len, NULL );
442        tr_sha1_to_hex( inf->hashString, inf->hash );
443        escape( inf->hashEscaped, inf->hash, SHA_DIGEST_LENGTH );
444
445        if( infoDictLength != NULL )
446            *infoDictLength = len;
447
448        if( infoDictOffset != NULL )
449        {
450            int mlen = 0;
451            char * mstr = tr_bencToStr( meta_in, TR_FMT_BENC, &mlen );
452            const char * offset = tr_memmem( mstr, mlen, bstr, len );
453            if( offset != NULL )
454                *infoDictOffset = offset - mstr;
455            tr_free( mstr );
456            if( offset == NULL )
457                return "info";
458        }
459
460        tr_free( bstr );
461    }
462
463    /* name */
464    if( !isMagnet ) {
465        if( !tr_bencDictFindStr( infoDict, "name.utf-8", &str ) )
466            if( !tr_bencDictFindStr( infoDict, "name", &str ) )
467                str = "";
468        if( !str || !*str )
469            return "name";
470        tr_free( inf->name );
471        inf->name = tr_utf8clean( str, -1, &err );
472    }
473
474    /* comment */
475    if( !tr_bencDictFindStr( meta, "comment.utf-8", &str ) )
476        if( !tr_bencDictFindStr( meta, "comment", &str ) )
477            str = "";
478    tr_free( inf->comment );
479    inf->comment = tr_utf8clean( str, -1, &err );
480
481    /* created by */
482    if( !tr_bencDictFindStr( meta, "created by.utf-8", &str ) )
483        if( !tr_bencDictFindStr( meta, "created by", &str ) )
484            str = "";
485    tr_free( inf->creator );
486    inf->creator = tr_utf8clean( str, -1, &err );
487
488    /* creation date */
489    if( !tr_bencDictFindInt( meta, "creation date", &i ) )
490        i = 0;
491    inf->dateCreated = i;
492
493    /* private */
494    if( !tr_bencDictFindInt( infoDict, "private", &i ) )
495        if( !tr_bencDictFindInt( meta, "private", &i ) )
496            i = 0;
497    inf->isPrivate = i != 0;
498
499    /* piece length */
500    if( !isMagnet ) {
501        if( !tr_bencDictFindInt( infoDict, "piece length", &i ) || ( i < 1 ) )
502            return "piece length";
503        inf->pieceSize = i;
504    }
505
506    /* pieces */
507    if( !isMagnet ) {
508        if( !tr_bencDictFindRaw( infoDict, "pieces", &raw, &raw_len ) )
509            return "pieces";
510        if( raw_len % SHA_DIGEST_LENGTH )
511            return "pieces";
512        inf->pieceCount = raw_len / SHA_DIGEST_LENGTH;
513        inf->pieces = tr_new0( tr_piece, inf->pieceCount );
514        for( i = 0; i < inf->pieceCount; ++i )
515            memcpy( inf->pieces[i].hash, &raw[i * SHA_DIGEST_LENGTH],
516                    SHA_DIGEST_LENGTH );
517    }
518
519    /* files */
520    if( !isMagnet ) {
521        if( ( str = parseFiles( inf, tr_bencDictFind( infoDict, "files" ),
522                                     tr_bencDictFind( infoDict, "length" ) ) ) )
523            return str;
524        if( !inf->fileCount || !inf->totalSize )
525            return "files";
526        if( (uint64_t) inf->pieceCount !=
527           ( inf->totalSize + inf->pieceSize - 1 ) / inf->pieceSize )
528            return "files";
529    }
530
531    /* get announce or announce-list */
532    if( ( str = getannounce( inf, meta ) ) )
533        return str;
534
535    /* get the url-list */
536    geturllist( inf, meta );
537
538    /* filename of Transmission's copy */
539    tr_free( inf->torrent );
540    inf->torrent = session ?  getTorrentFilename( session, inf ) : NULL;
541
542    return NULL;
543}
544
545tr_bool
546tr_metainfoParse( const tr_session * session,
547                  const tr_benc    * meta_in,
548                  tr_info          * inf,
549                  tr_bool          * hasInfoDict,
550                  int              * infoDictOffset,
551                  int              * infoDictLength )
552{
553    const char * badTag = tr_metainfoParseImpl( session,
554                                                inf,
555                                                hasInfoDict,
556                                                infoDictOffset,
557                                                infoDictLength,
558                                                meta_in );
559    const tr_bool success = badTag == NULL;
560
561    if( badTag )
562    {
563        tr_nerr( inf->name, _( "Invalid metadata entry \"%s\"" ), badTag );
564        tr_metainfoFree( inf );
565    }
566
567    return success;
568}
569
570void
571tr_metainfoFree( tr_info * inf )
572{
573    int i;
574    tr_file_index_t ff;
575
576    for( i = 0; i < inf->webseedCount; ++i )
577        tr_free( inf->webseeds[i] );
578
579    for( ff = 0; ff < inf->fileCount; ++ff )
580        tr_free( inf->files[ff].name );
581
582    tr_free( inf->webseeds );
583    tr_free( inf->pieces );
584    tr_free( inf->files );
585    tr_free( inf->comment );
586    tr_free( inf->creator );
587    tr_free( inf->torrent );
588    tr_free( inf->name );
589
590    for( i = 0; i < inf->trackerCount; ++i )
591    {
592        tr_free( inf->trackers[i].announce );
593        tr_free( inf->trackers[i].scrape );
594    }
595    tr_free( inf->trackers );
596
597    memset( inf, '\0', sizeof( tr_info ) );
598}
599
600void
601tr_metainfoRemoveSaved( const tr_session * session,
602                        const tr_info *   inf )
603{
604    char * filename;
605
606    filename = getTorrentFilename( session, inf );
607    unlink( filename );
608    tr_free( filename );
609
610    filename = getOldTorrentFilename( session, inf );
611    unlink( filename );
612    tr_free( filename );
613}
Note: See TracBrowser for help on using the repository browser.