/* * This file Copyright (C) Mnemosyne LLC * * This file is licensed by the GPL version 2. Works owned by the * Transmission project are granted a special exemption to clause 2(b) * so that the bulk of its code can remain under the MIT license. * This exemption does not extend to derived works not owned by * the Transmission project. * * $Id: metainfo.c 12848 2011-09-06 16:45:48Z jordan $ */ #include #include #include /* fopen(), fwrite(), fclose() */ #include /* strlen() */ #include #include #include /* unlink, stat */ #include #include "transmission.h" #include "session.h" #include "bencode.h" #include "crypto.h" /* tr_sha1 */ #include "metainfo.h" #include "platform.h" /* tr_getTorrentDir() */ #include "utils.h" /*** **** ***/ char* tr_metainfoGetBasename( const tr_info * inf ) { size_t i; const size_t name_len = strlen( inf->name ); char * ret = tr_strdup_printf( "%s.%16.16s", inf->name, inf->hashString ); for( i=0; ihashString, tags[i] ); if( !stat( path, &sb ) && ( ( sb.st_mode & S_IFMT ) == S_IFREG ) ) return path; tr_free( path ); } /* test a non-tagged file */ path = tr_buildPath( tr_getTorrentDir( session ), inf->hashString, NULL ); if( !stat( path, &sb ) && ( ( sb.st_mode & S_IFMT ) == S_IFREG ) ) return path; tr_free( path ); /* return the -gtk form by default, since that's the most common case. don't bother testing stat() on it since this is the last candidate and we don't want to return NULL anyway */ return tr_strdup_printf( "%s%c%s-%s", tr_getTorrentDir( session ), '/', inf->hashString, "gtk" ); } /* this is for really old versions of T and will probably be removed someday */ void tr_metainfoMigrate( tr_session * session, tr_info * inf ) { struct stat new_sb; char * name = getTorrentFilename( session, inf ); if( stat( name, &new_sb ) || ( ( new_sb.st_mode & S_IFMT ) != S_IFREG ) ) { char * old_name = getOldTorrentFilename( session, inf ); size_t contentLen; uint8_t * content; tr_mkdirp( tr_getTorrentDir( session ), 0777 ); if( ( content = tr_loadFile( old_name, &contentLen ) ) ) { FILE * out; errno = 0; out = fopen( name, "wb+" ); if( !out ) { tr_nerr( inf->name, _( "Couldn't create \"%1$s\": %2$s" ), name, tr_strerror( errno ) ); } else { if( fwrite( content, sizeof( uint8_t ), contentLen, out ) == contentLen ) { tr_free( inf->torrent ); inf->torrent = tr_strdup( name ); tr_sessionSetTorrentFile( session, inf->hashString, name ); unlink( old_name ); } fclose( out ); } } tr_free( content ); tr_free( old_name ); } tr_free( name ); } /*** **** ***/ static bool path_is_suspicious( const char * path ) { return ( path == NULL ) || ( strstr( path, "../" ) != NULL ); } static bool getfile( char ** setme, const char * root, tr_benc * path, struct evbuffer * buf ) { bool success = false; if( tr_bencIsList( path ) ) { int i; const int n = tr_bencListSize( path ); evbuffer_drain( buf, evbuffer_get_length( buf ) ); evbuffer_add( buf, root, strlen( root ) ); for( i = 0; i < n; ++i ) { const char * str; if( tr_bencGetStr( tr_bencListChild( path, i ), &str ) ) { evbuffer_add( buf, TR_PATH_DELIMITER_STR, 1 ); evbuffer_add( buf, str, strlen( str ) ); } } *setme = tr_utf8clean( (char*)evbuffer_pullup( buf, -1 ), evbuffer_get_length( buf ) ); /* fprintf( stderr, "[%s]\n", *setme ); */ success = true; } if( ( *setme != NULL ) && path_is_suspicious( *setme ) ) { tr_free( *setme ); *setme = NULL; success = false; } return success; } static const char* parseFiles( tr_info * inf, tr_benc * files, const tr_benc * length ) { int64_t len; inf->totalSize = 0; if( tr_bencIsList( files ) ) /* multi-file mode */ { tr_file_index_t i; struct evbuffer * buf = evbuffer_new( ); inf->isMultifile = 1; inf->fileCount = tr_bencListSize( files ); inf->files = tr_new0( tr_file, inf->fileCount ); for( i = 0; i < inf->fileCount; ++i ) { tr_benc * file; tr_benc * path; file = tr_bencListChild( files, i ); if( !tr_bencIsDict( file ) ) return "files"; if( !tr_bencDictFindList( file, "path.utf-8", &path ) ) if( !tr_bencDictFindList( file, "path", &path ) ) return "path"; if( !getfile( &inf->files[i].name, inf->name, path, buf ) ) return "path"; if( !tr_bencDictFindInt( file, "length", &len ) ) return "length"; inf->files[i].length = len; inf->totalSize += len; } evbuffer_free( buf ); } else if( tr_bencGetInt( length, &len ) ) /* single-file mode */ { if( path_is_suspicious( inf->name ) ) return "path"; inf->isMultifile = 0; inf->fileCount = 1; inf->files = tr_new0( tr_file, 1 ); inf->files[0].name = tr_strdup( inf->name ); inf->files[0].length = len; inf->totalSize += len; } else { return "length"; } return NULL; } static char * tr_convertAnnounceToScrape( const char * announce ) { char * scrape = NULL; const char * s; /* To derive the scrape URL use the following steps: * Begin with the announce URL. Find the last '/' in it. * If the text immediately following that '/' isn't 'announce' * it will be taken as a sign that that tracker doesn't support * the scrape convention. If it does, substitute 'scrape' for * 'announce' to find the scrape page. */ if( ( ( s = strrchr( announce, '/' ) ) ) && !strncmp( ++s, "announce", 8 ) ) { const char * prefix = announce; const size_t prefix_len = s - announce; const char * suffix = s + 8; const size_t suffix_len = strlen( suffix ); const size_t alloc_len = prefix_len + 6 + suffix_len + 1; char * walk = scrape = tr_new( char, alloc_len ); memcpy( walk, prefix, prefix_len ); walk += prefix_len; memcpy( walk, "scrape", 6 ); walk += 6; memcpy( walk, suffix, suffix_len ); walk += suffix_len; *walk++ = '\0'; assert( walk - scrape == (int)alloc_len ); } return scrape; } static const char* getannounce( tr_info * inf, tr_benc * meta ) { const char * str; tr_tracker_info * trackers = NULL; int trackerCount = 0; tr_benc * tiers; /* Announce-list */ if( tr_bencDictFindList( meta, "announce-list", &tiers ) ) { int n; int i, j, validTiers; const int numTiers = tr_bencListSize( tiers ); n = 0; for( i = 0; i < numTiers; ++i ) n += tr_bencListSize( tr_bencListChild( tiers, i ) ); trackers = tr_new0( tr_tracker_info, n ); for( i = 0, validTiers = 0; i < numTiers; ++i ) { tr_benc * tier = tr_bencListChild( tiers, i ); const int tierSize = tr_bencListSize( tier ); bool anyAdded = false; for( j = 0; j < tierSize; ++j ) { if( tr_bencGetStr( tr_bencListChild( tier, j ), &str ) ) { char * url = tr_strstrip( tr_strdup( str ) ); if( !tr_urlIsValidTracker( url ) ) tr_free( url ); else { tr_tracker_info * t = trackers + trackerCount; t->tier = validTiers; t->announce = url; t->scrape = tr_convertAnnounceToScrape( url ); t->id = trackerCount; anyAdded = true; ++trackerCount; } } } if( anyAdded ) ++validTiers; } /* did we use any of the tiers? */ if( !trackerCount ) { tr_free( trackers ); trackers = NULL; } } /* Regular announce value */ if( !trackerCount && tr_bencDictFindStr( meta, "announce", &str ) ) { char * url = tr_strstrip( tr_strdup( str ) ); if( !tr_urlIsValidTracker( url ) ) tr_free( url ); else { trackers = tr_new0( tr_tracker_info, 1 ); trackers[trackerCount].tier = 0; trackers[trackerCount].announce = url; trackers[trackerCount].scrape = tr_convertAnnounceToScrape( url ); trackers[trackerCount].id = 0; trackerCount++; /*fprintf( stderr, "single announce: [%s]\n", url );*/ } } inf->trackers = trackers; inf->trackerCount = trackerCount; return NULL; } /** * @brief Ensure that the URLs for multfile torrents end in a slash. * * See http://bittorrent.org/beps/bep_0019.html#metadata-extension * for background on how the trailing slash is used for "url-list" * fields. * * This function is to workaround some .torrent generators, such as * mktorrent and very old versions of utorrent, that don't add the * trailing slash for multifile torrents if omitted by the end user. */ static char* fix_webseed_url( const tr_info * inf, const char * url ) { char * ret = NULL; const size_t len = strlen( url ); if( tr_urlIsValid( url, len ) ) { if( ( inf->fileCount > 1 ) && ( len > 0 ) && ( url[len-1] != '/' ) ) ret = tr_strdup_printf( "%*.*s/", (int)len, (int)len, url ); else ret = tr_strndup( url, len ); } return ret; } static void geturllist( tr_info * inf, tr_benc * meta ) { tr_benc * urls; const char * url; if( tr_bencDictFindList( meta, "url-list", &urls ) ) { int i; const int n = tr_bencListSize( urls ); inf->webseedCount = 0; inf->webseeds = tr_new0( char*, n ); for( i = 0; i < n; ++i ) { if( tr_bencGetStr( tr_bencListChild( urls, i ), &url ) ) { char * fixed_url = fix_webseed_url( inf, url ); if( fixed_url != NULL ) inf->webseeds[inf->webseedCount++] = fixed_url; } } } else if( tr_bencDictFindStr( meta, "url-list", &url ) ) /* handle single items in webseeds */ { char * fixed_url = fix_webseed_url( inf, url ); if( fixed_url != NULL ) { inf->webseedCount = 1; inf->webseeds = tr_new0( char*, 1 ); inf->webseeds[0] = fixed_url; } } } static const char* tr_metainfoParseImpl( const tr_session * session, tr_info * inf, bool * hasInfoDict, int * infoDictLength, const tr_benc * meta_in ) { int64_t i; size_t raw_len; const char * str; const uint8_t * raw; tr_benc * d; tr_benc * infoDict = NULL; tr_benc * meta = (tr_benc *) meta_in; bool b; bool isMagnet = false; /* info_hash: urlencoded 20-byte SHA1 hash of the value of the info key * from the Metainfo file. Note that the value will be a bencoded * dictionary, given the definition of the info key above. */ b = tr_bencDictFindDict( meta, "info", &infoDict ); if( hasInfoDict != NULL ) *hasInfoDict = b; if( !b ) { /* no info dictionary... is this a magnet link? */ if( tr_bencDictFindDict( meta, "magnet-info", &d ) ) { isMagnet = true; /* get the info-hash */ if( !tr_bencDictFindRaw( d, "info_hash", &raw, &raw_len ) ) return "info_hash"; if( raw_len != SHA_DIGEST_LENGTH ) return "info_hash"; memcpy( inf->hash, raw, raw_len ); tr_sha1_to_hex( inf->hashString, inf->hash ); /* maybe get the display name */ if( tr_bencDictFindStr( d, "display-name", &str ) ) { tr_free( inf->name ); inf->name = tr_strdup( str ); } if( !inf->name ) inf->name = tr_strdup( inf->hashString ); } else /* not a magnet link and has no info dict... */ { return "info"; } } else { int len; char * bstr = tr_bencToStr( infoDict, TR_FMT_BENC, &len ); tr_sha1( inf->hash, bstr, len, NULL ); tr_sha1_to_hex( inf->hashString, inf->hash ); if( infoDictLength != NULL ) *infoDictLength = len; tr_free( bstr ); } /* name */ if( !isMagnet ) { if( !tr_bencDictFindStr( infoDict, "name.utf-8", &str ) ) if( !tr_bencDictFindStr( infoDict, "name", &str ) ) str = ""; if( !str || !*str ) return "name"; tr_free( inf->name ); inf->name = tr_utf8clean( str, -1 ); } /* comment */ if( !tr_bencDictFindStr( meta, "comment.utf-8", &str ) ) if( !tr_bencDictFindStr( meta, "comment", &str ) ) str = ""; tr_free( inf->comment ); inf->comment = tr_utf8clean( str, -1 ); /* created by */ if( !tr_bencDictFindStr( meta, "created by.utf-8", &str ) ) if( !tr_bencDictFindStr( meta, "created by", &str ) ) str = ""; tr_free( inf->creator ); inf->creator = tr_utf8clean( str, -1 ); /* creation date */ if( !tr_bencDictFindInt( meta, "creation date", &i ) ) i = 0; inf->dateCreated = i; /* private */ if( !tr_bencDictFindInt( infoDict, "private", &i ) ) if( !tr_bencDictFindInt( meta, "private", &i ) ) i = 0; inf->isPrivate = i != 0; /* piece length */ if( !isMagnet ) { if( !tr_bencDictFindInt( infoDict, "piece length", &i ) || ( i < 1 ) ) return "piece length"; inf->pieceSize = i; } /* pieces */ if( !isMagnet ) { if( !tr_bencDictFindRaw( infoDict, "pieces", &raw, &raw_len ) ) return "pieces"; if( raw_len % SHA_DIGEST_LENGTH ) return "pieces"; inf->pieceCount = raw_len / SHA_DIGEST_LENGTH; inf->pieces = tr_new0( tr_piece, inf->pieceCount ); for( i = 0; i < inf->pieceCount; ++i ) memcpy( inf->pieces[i].hash, &raw[i * SHA_DIGEST_LENGTH], SHA_DIGEST_LENGTH ); } /* files */ if( !isMagnet ) { if( ( str = parseFiles( inf, tr_bencDictFind( infoDict, "files" ), tr_bencDictFind( infoDict, "length" ) ) ) ) return str; if( !inf->fileCount || !inf->totalSize ) return "files"; if( (uint64_t) inf->pieceCount != ( inf->totalSize + inf->pieceSize - 1 ) / inf->pieceSize ) return "files"; } /* get announce or announce-list */ if( ( str = getannounce( inf, meta ) ) ) return str; /* get the url-list */ geturllist( inf, meta ); /* filename of Transmission's copy */ tr_free( inf->torrent ); inf->torrent = session ? getTorrentFilename( session, inf ) : NULL; return NULL; } bool tr_metainfoParse( const tr_session * session, const tr_benc * meta_in, tr_info * inf, bool * hasInfoDict, int * infoDictLength ) { const char * badTag = tr_metainfoParseImpl( session, inf, hasInfoDict, infoDictLength, meta_in ); const bool success = badTag == NULL; if( badTag ) { tr_nerr( inf->name, _( "Invalid metadata entry \"%s\"" ), badTag ); tr_metainfoFree( inf ); } return success; } void tr_metainfoFree( tr_info * inf ) { int i; tr_file_index_t ff; for( i = 0; i < inf->webseedCount; ++i ) tr_free( inf->webseeds[i] ); for( ff = 0; ff < inf->fileCount; ++ff ) tr_free( inf->files[ff].name ); tr_free( inf->webseeds ); tr_free( inf->pieces ); tr_free( inf->files ); tr_free( inf->comment ); tr_free( inf->creator ); tr_free( inf->torrent ); tr_free( inf->name ); for( i = 0; i < inf->trackerCount; ++i ) { tr_free( inf->trackers[i].announce ); tr_free( inf->trackers[i].scrape ); } tr_free( inf->trackers ); memset( inf, '\0', sizeof( tr_info ) ); } void tr_metainfoRemoveSaved( const tr_session * session, const tr_info * inf ) { char * filename; filename = getTorrentFilename( session, inf ); unlink( filename ); tr_free( filename ); filename = getOldTorrentFilename( session, inf ); unlink( filename ); tr_free( filename ); }