/* * 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$ */ #include #include #include /* fopen (), fwrite (), fclose () */ #include /* strlen () */ #include #include /* unlink, stat */ #include #include "transmission.h" #include "session.h" #include "crypto.h" /* tr_sha1 */ #include "metainfo.h" #include "platform.h" /* tr_getTorrentDir () */ #include "utils.h" #include "variant.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; itotalSize = 0; if (tr_variantIsList (files)) /* multi-file mode */ { tr_file_index_t i; struct evbuffer * buf = evbuffer_new (); inf->isMultifile = 1; inf->fileCount = tr_variantListSize (files); inf->files = tr_new0 (tr_file, inf->fileCount); for (i=0; ifileCount; i++) { tr_variant * file; tr_variant * path; file = tr_variantListChild (files, i); if (!tr_variantIsDict (file)) return "files"; if (!tr_variantDictFindList (file, "path.utf-8", &path)) if (!tr_variantDictFindList (file, "path", &path)) return "path"; if (!getfile (&inf->files[i].name, inf->name, path, buf)) return "path"; if (!tr_variantDictFindInt (file, "length", &len)) return "length"; inf->files[i].length = len; inf->totalSize += len; } evbuffer_free (buf); } else if (tr_variantGetInt (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); } /* Some torrents with UDP annouce URLs don't have /announce. */ else if (!strncmp (announce, "udp:", 4)) { scrape = tr_strdup (announce); } return scrape; } static const char* getannounce (tr_info * inf, tr_variant * meta) { size_t len; const char * str; tr_tracker_info * trackers = NULL; int trackerCount = 0; tr_variant * tiers; /* Announce-list */ if (tr_variantDictFindList (meta, "announce-list", &tiers)) { int n; int i, j, validTiers; const int numTiers = tr_variantListSize (tiers); n = 0; for (i=0; itier = 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_variantDictFindStr (meta, "announce", &str, &len)) { char * url = tr_strstrip (tr_strndup (str, len)); 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_in) { size_t len; char * url; char * ret = NULL; url = tr_strdup (url_in); tr_strstrip (url); 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); } tr_free (url); return ret; } static void geturllist (tr_info * inf, tr_variant * meta) { tr_variant * urls; const char * url; if (tr_variantDictFindList (meta, "url-list", &urls)) { int i; const int n = tr_variantListSize (urls); inf->webseedCount = 0; inf->webseeds = tr_new0 (char*, n); for (i=0; iwebseeds[inf->webseedCount++] = fixed_url; } } } else if (tr_variantDictFindStr (meta, "url-list", &url, NULL)) /* 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_variant * meta_in) { int64_t i; size_t len; const char * str; const uint8_t * raw; tr_variant * d; tr_variant * infoDict = NULL; tr_variant * meta = (tr_variant *) 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_variantDictFindDict (meta, "info", &infoDict); if (hasInfoDict != NULL) *hasInfoDict = b; if (!b) { /* no info dictionary... is this a magnet link? */ if (tr_variantDictFindDict (meta, "magnet-info", &d)) { isMagnet = true; /* get the info-hash */ if (!tr_variantDictFindRaw (d, "info_hash", &raw, &len)) return "info_hash"; if (len != SHA_DIGEST_LENGTH) return "info_hash"; memcpy (inf->hash, raw, len); tr_sha1_to_hex (inf->hashString, inf->hash); /* maybe get the display name */ if (tr_variantDictFindStr (d, "display-name", &str, &len)) { tr_free (inf->name); inf->name = tr_strndup (str, len); } 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_variantToStr (infoDict, TR_VARIANT_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) { len = 0; if (!tr_variantDictFindStr (infoDict, "name.utf-8", &str, &len)) if (!tr_variantDictFindStr (infoDict, "name", &str, &len)) str = ""; if (!str || !*str) return "name"; tr_free (inf->name); inf->name = tr_utf8clean (str, len); } /* comment */ len = 0; if (!tr_variantDictFindStr (meta, "comment.utf-8", &str, &len)) if (!tr_variantDictFindStr (meta, "comment", &str, &len)) str = ""; tr_free (inf->comment); inf->comment = tr_utf8clean (str, len); /* created by */ len = 0; if (!tr_variantDictFindStr (meta, "created by.utf-8", &str, &len)) if (!tr_variantDictFindStr (meta, "created by", &str, &len)) str = ""; tr_free (inf->creator); inf->creator = tr_utf8clean (str, len); /* creation date */ if (!tr_variantDictFindInt (meta, "creation date", &i)) i = 0; inf->dateCreated = i; /* private */ if (!tr_variantDictFindInt (infoDict, "private", &i)) if (!tr_variantDictFindInt (meta, "private", &i)) i = 0; inf->isPrivate = i != 0; /* piece length */ if (!isMagnet) { if (!tr_variantDictFindInt (infoDict, "piece length", &i) || (i < 1)) return "piece length"; inf->pieceSize = i; } /* pieces */ if (!isMagnet) { if (!tr_variantDictFindRaw (infoDict, "pieces", &raw, &len)) return "pieces"; if (len % SHA_DIGEST_LENGTH) return "pieces"; inf->pieceCount = len / SHA_DIGEST_LENGTH; inf->pieces = tr_new0 (tr_piece, inf->pieceCount); for (i=0; ipieceCount; i++) memcpy (inf->pieces[i].hash, &raw[i * SHA_DIGEST_LENGTH], SHA_DIGEST_LENGTH); } /* files */ if (!isMagnet) { if ((str = parseFiles (inf, tr_variantDictFind (infoDict, "files"), tr_variantDictFind (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_variant * 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; iwebseedCount; i++) tr_free (inf->webseeds[i]); for (ff=0; fffileCount; 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; itrackerCount; 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); }