How to Create a Torrent Tracker: Part 2 - Upload Form

July 15, 2009 by Hangman

In the previous part of this guide, we have seen how to run the XBTT tracker on our server and built a basic torrent download system with passkey support. Now we need a way to upload our torrents, so in this second part we’ll see how to create a fully working upload form featuring validation of metainfo .torrent files, including checks such as MIME type, private flag, announce URL, and torrent hash to detect duplicates.

A look at metafile specification

As we already know, a torrent file is a bencoded dictionary containing meta information such as torrent name, announce URL of the tracker, file list including names and sizes, creation date and other values. On wiki.theory.org there’s a full structure specification of torrent meta files, so be sure to check it out.

If we are building a private tracker, in the upload process we will need to check some values in order to validate the torrent file:

  • announce: string containing the announce URL of the tracker
  • private: an integer which, when set to 1, will prevent clients to obtain peers from sources other than the specified tracker (such as PEX or DHT).
  • files: list containing information about files. This allows us to check for a “Torrent downloaded from” text file (a la Demonoid.com)

In order to validate a torrent file, we need a bencode library. If you’ve played around with the previous tutorial, you may already have found your favourite one. Anyway, in code examples we will refer to this extension: http://www.pear.php.net/package/File_Bittorrent2/
To use this, you will need to install PEAR on your system first. On Debian (or derivatives such as Ubuntu) it’s a matter of a simple command:

sudo apt-get install php-pear

Now just type this to install the File_Bittorrent2 package:

pear install File_Bittorrent2

If all went fine, our PHP scripts should now be able to access the functions of this library.

Setting up the form

For the sake of this tutorial, our upload form will be extremely simple and will contain only the necessary fields. Obviously in a real world form you’d want to include fields such as torrent name, description, drop-down list of categories and so on.
Here’s the HTML code for our quick and simple form:

<form action="upload-torrent.php" action="post" enctype="multipart/form-data">
        <input type="hidden" name="MAX_FILE_SIZE" value="1000000" />
        <input type="file" name="torrent" />
        <input type="submit" value="Upload" />
</form>

Obviously in the upload-torrent.php script we’re going to handle validation, and if the file is valid, we will copy the uploaded file in a folder on the server.
Once the upload is complete, we will have access to information about the file through the $_FILES global. The first things we want to check are the file’s extension and size.

<?php
        define(‘MAX_FILE_SIZE’,1000000); // maximum size we allow, in bytes

        if($_FILES[‘torrent’][’size’] >= MAX_FILE_SIZE)
                die(‘This torrent file is too big’);

        $ext=pathinfo($_FILES[‘torrent’][‘name’],PATHINFO_EXTENSION);
        if($ext!=‘.torrent’)
                die(‘Invalid extension’);
?>

Some trackers let you upload torrents with other extensions such as txt or png to bypass ISP checks. If this is your intention, you may want to skip the extension validation or edit the last if statements to fit your needs.

PHP gives us information about the file MIME type, although this is actually provided by the browser, and this sounds unreliable. To check the MIME type on the server, we can use the Fileinfo extension. As of PHP 5.3.0, this is installed by default. If you use an older version, just install it with PEAR using this command:

pear install fileinfo

Of course Fileinfo needs a handle to the file in order to check its type. But, at this point, the file is in a temporary location on our server, until we move it into another directory.
This location is stored in the tmp_name element of the file array.

<?php
        define(‘TORRENT_MIMETYPE’,‘application/x-bittorrent’);

        // check file mime type (must be application/x-bittorrent)
        $handle = finfo_open(FILEINFO_MIME);
        $mime_type = finfo_file($handle,$_FILES[‘torrent’][‘tmp_name’]);

        if($mime_type!=TORRENT_MIMETYPE)
                die(‘Invalid MIME type: ‘.$mime_type);
?>

Now that we are sure we’re actually dealing with a torrent file, we are ready to validate its content.
We’re going to use File_Bittorrent, so see the previous paragraph if you haven’t installed it yet.

<?php
        define(‘ANNOUNCE_URL’,‘http://example.com:2710/announce’);

        $dec=new File_Bittorrent2_Decode;
        $info=$dec->decodeFile($_FILES[‘torrent’][‘tmp_name’]);

        // check if a torrent with this info_hash already exists
        $exists=mysql_fetch_row(mysql_query("SELECT * FROM xbt_files WHERE LOWER(hex(`info_hash`))=’{$info['info_hash']}’"));
        if(!empty($exists))
                die(‘This torrent has already been uploaded’);

        // private tracker: the announce URL must match the one of our tracker
        if($info[‘announce’]!=ANNOUNCE_URL)
                die(‘Sorry, but the announce URL must be ‘.ANNOUNCE_URL);

        // private tracker: if we force the uploader to enable the private flag, our tracker will be the only source for peers
        if(!$dec->isPrivate())
                die(‘This torrent is not private. Please set the private flag.’);
?>

At this point we may want to do the “Torrent downloaded from” info-file check. We just loop through each file in the torrent until we have found a filename that matches our constant string. Also, we check for the file size.

<?php
        define(‘INFOFILE_FILENAME’,‘Torrent downloaded from example.com.txt’);
        define(‘INFOFILE_CONTENT’,‘Torrent downloaded from example.com);

        $infofile_found=false;
        foreach($info[‘files’] as $file)
        {
                if($file[‘filename’]==INFOFILE_FILENAME)
                {
                        if($file[’size’]==strlen(INFOFILE_CONTENT))
                        {
                                $infofile_found=true;
                                break;
                        }
                }
        }

        if($infofile_found===false)
                die(‘This torrent does not contain our info-file. Be sure to add it into the root of your torrent.’);

?>

Uploading and inserting into tracker database

Now we’re ready to move the torrent from its temporary location to a directory where we will store all the uploaded files. Also, we will insert a new record on the XBTT database to start tracking it.
Since we will need to easily access the torrent file in the future (e.g. in details pages), we’re going to rename it after the torrent ID. For example, if the torrent’s record index on the database is 1, the filename will be “1.torrent”. In this way it will be easy to get information from the file in PHP scripts given the torrent ID (this will also keep the filesystem clean).

<?php
        define(‘TORRENT_UPLOAD_PATH’,‘uploads’);
        define(‘UPLOADED_TORRENT_EXTENSION’,‘.torrent’);

        $filename=basename($_FILES[‘torrent’][‘name’]);

        // get next auto_increment value in the files database
        $row=mysql_fetch_array(mysql_query("SHOW TABLE STATUS LIKE ‘xbt_files’"));
        $next_increment = $row[‘Auto_increment’];

        // target path (torrent uploads directory + next id in files table + torrent ext)
        $target_path = TORRENT_UPLOAD_PATH.‘/’.$next_increment.UPLOADED_TORRENT_EXTENSION;

        // move the file from the temp location to the target path
        if(!move_uploaded_file($_FILES[‘torrent’][‘tmp_name’], $target_path))
                die(‘There was an error trying to upload your torrent. Please try again!’);

        // it’s time to add a record for the torrent into the database
        if(!mysql_query("INSERT INTO xbt_files (info_hash,mtime,ctime) VALUES(X{$info['info_hash']},UNIX_TIMESTAMP(),UNIX_TIMESTAMP())"))
        {
                die(‘There was an error trying to add your torrent.’);

                // delete the file
                unlink($target_path);
        }

        // get the last inserted id
        $torrent_id=mysql_insert_id();

        // show a success message, maybe including an URL to a details page using the torrent id
        echo ‘Your torrent was uploaded successfully.’;
?>

Here we go. First, we store the target path of the file, which is composed by the torrent uploads path, the next ID in the files table (as we’ve chosen to rename torrent files using IDs) plus the torrent extension. Then we use the move_uploaded_file function to move the file to our target location.

Once this is done, we want to insert the torrent in the XBTT files table, so that the tracker will recognize it when clients request it (don’t forget that if the auto_register option is enabled, XBTT will insert the record automatically for any torrent). Notice that we delete the uploaded file if the insert query fails.
Keep in mind that after we have inserted the new torrent, we will have to wait the XBTT db check interval time (15 seconds by default) for it to be actually tracked.

NOTE: If our site handles user management, we may want to add the uploader’s user ID into the insert query. Of course you will need to add a new INT field in your users table.

Deleting the torrent

What if an user uploaded the wrong torrent and we want to provide a way to delete it?
It’s not difficult at all. It’s just a matter of telling XBTT not to track that specific torrent anymore. This is done setting the flags field. Also, we will delete the torrent metafile from the filesystem. The ID filename thing comes in handy for this purpose.

<?php
        mysql_query("UPDATE xbt_files SET flags=1 WHERE fid={$torrent_id}");

        $file=TORRENT_UPLOAD_PATH.‘/’.$torrent_id.UPLOADED_TORRENT_EXTENSION;
        if(file_exists($file))
                unlink($file);
?>

In the next database update, XBTT should automatically remove the record and stop tracking the torrent.

Conclusion

In this second part of the series, we’ve seen how to build a basic torrent upload form, featuring various kinds of validation. Also, we’ve seen how to insert a torrent into the tracker database and how to properly remove it.

Stay tuned and see you in the next tutorial!