VB.NET: Torrentdatei einlesen, announce durchführen und IPs auslesen

Mo, 26.04.2010 - 11:34 -- admin

Ähnlich wie mit PHP BitTorrent Announce Client wollte ich auch einmal eine BitTorrent Datei mit VB.NET einlesen und sämtliche Info der Torrentdatei parsen, wie z.B. Announceurl, enthaltene Dateiinformationen und den Hashwert. Da sämtliche Dateien und Kommunikation innerhalb von Bittorent bencoded werden, muss man die Info entsprechend vorher "entschlüsseln" bzw konvertieren.
Hat man alle Informationen einer Torrentdatei zusammen kann man dann direkt in VB.NET anhand der Torrentdatei einen Torrentclient (z.B. BitTornado, uTorrent) simulieren und eine Anfrage an den Tracker schicken. So erhält man dann eine Info über die aktuell vorhanden Seeder und Leecher mit der IP und Port in einer Peerliste. Auch hier wird wieder alles bencoded also, muss man natürlich auch wieder etwas Hand anlegen.

MonoTorrent

Gott sein Dank gibt es mittlerweile eine recht gute .NET Library die alles nötige enthält. Mit MonoTorrent kann man neben einem BitTorrent-Client auch gleich einen ganzen Tracker-Server innerhalb von .NET betreiben. Leider sind deswegen die Funktionen Richtung Komplettlösung zu versehen und lassen sich zum Teil nicht einzeln aufrufen und im eigenen Programmen nutzen. Wir müssen also einige Funktion aus der Library abspalten und etwas anpassen, damit wir sie nutzen können.

Beispiel

Im unten aufgeführten Beispiel lade ich eine beliebige Torrentdatei. Anhand der enthaltenen Announce Adresse simuliert das Programm eine BitTornado Anfrage an den Tracker und gibt die Peerlist aus. Um das Programm relativ einfach zuhalten wird lediglich die erste Trackeradresse angefragt und diese muss zugleich eine http Adresse enthalten.
Mittlerweile können Trackeranfragen auch über das udp abgewickelt werden, ist allerdings nicht mehr so transparent und wird von den meisten Trackern nicht unterstützt.

Imports MonoTorrent.BEncoding
Imports MonoTorrent.Client
Imports MonoTorrent.Client.Tracker
Imports MonoTorrent.Common
 
Imports System.IO
Imports System.Net
 
Module BitTorrentAnnounceReader
    Sub main()
 
        'load Torrentfile
        Dim TorrentFile As Torrent = Torrent.Load("D:\youtorrent.torrent")
        Console.WriteLine(TorrentFile.Name)
 
        'generate annouce paramters
        'since we simluate a BitTornado Client double check PeerID and Key Paramter
        Dim AnnounceParameter As New AnnounceParameters
        AnnounceParameter.BytesLeft = TorrentFile.Size
        AnnounceParameter.BytesUploaded = 0
        AnnounceParameter.BytesDownloaded = 0
        AnnounceParameter.Port = 12224
        AnnounceParameter.InfoHash = TorrentFile.InfoHash
        AnnounceParameter.PeerId = "T03I-----" & GenerateTorrentClientKeys(11, 1)
        AnnounceParameter.ClientEvent = TorrentEvent.Started
 
        'a torrentfile can have more than one url, we use only the first one
        'the url should have http to work for this example
        Dim AnnounceUrl As String = TorrentFile.AnnounceUrls.Item(0).Item(0).ToString
        Console.WriteLine(AnnounceUrl)
 
        'the full announceurl that will fired to tracker
        'we are simulating a BitTorando Client
        Dim FullAnnounceUrl As String = CreateAnnounceString(AnnounceParameter, AnnounceUrl, GenerateTorrentClientKeys(6))
 
        'building a webrequest for tracker request; some silly line look at comments on
        'MonoTorrent.Client.Tracker.HTTPTracker.Announce
        Dim req As HttpWebRequest = CType(WebRequest.Create(FullAnnounceUrl), HttpWebRequest)
        req.KeepAlive = False
        req.Proxy = New WebProxy
 
        'we want to simulate a BitTornado Client, so http headers
        req.UserAgent = "User-Agent: BitTornado/T-0.3.18"
 
        'to simulate full client we need also gzip but for better usage we dont use it
        ' req.Headers.Add("Accept-Encoding", "gzip")
        ' If (resp.ContentEncoding.ToLower().Contains("gzip")) Then
        ' Str = New IO.Compression.GZipStream(Str, IO.Compression.CompressionMode.Decompress)
 
        Dim response As HttpWebResponse = req.GetResponse
        Dim fs As Stream = WebResponseToStream(response)
 
        Dim peers As List(Of Peer) = AnnounceGetPeerList(fs)
        Console.WriteLine("Tracker returned:" & peers.Count)
        For Each PeerInfo As Peer In peers
            Console.WriteLine(PeerInfo.ConnectionUri.Host & ":" & PeerInfo.ConnectionUri.Port)
        Next
 
        Console.ReadKey()
    End Sub
    ''' <summary>
    ''' Generate a random key depending on which TorrentClient to simulate
    ''' </summary>
    ''' <param name="len"></param>
    ''' <param name="keys"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Function GenerateTorrentClientKeys(ByVal len As Integer, Optional ByVal keys As Integer = 1) As String
 
        Dim Chars() As String = {"abcdefghijklmnopqrstuvwxyz0123456789", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"}
        'Chars[0] = uTorrent Keys
        'Chars[1] = BitTornado Keys
        Dim str As String = ""
        Dim r As New System.Random()
 
        For i = 1 To len
            str &= Chars(keys).Substring(r.Next(1, Chars(keys).Length), 1)
        Next
 
        Return str
 
    End Function
    ''' <summary>
    ''' A simple way of parsing a bencoded peerlist. it will autodecode compact mode request
    ''' you can find the full announce parser in the orignal file:
    ''' 
    ''' MonoTorrent.Client.Tracker.HTTPTracker.HandleAnnounce
    ''' http://anonsvn.mono-project.com/viewvc/trunk/bitsharp/src/MonoTorrent/MonoTorrent.Client/Tracker/HTTPTracker.cs
    ''' </summary>
    ''' <param name="fs"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Function AnnounceGetPeerList(ByVal fs As Stream) As List(Of Peer)
        'you can also use a file with a torrenrequest
        'Dim reader As New RawReader(IO.File.Open("file.req", IO.FileMode.Open), False)
 
        'decode the bencoded stream
        Dim dictionary As BEncodedDictionary = BEncodedValue.Decode(Of BEncodedDictionary)(fs)
 
        Dim peers As New List(Of Peer)
        For Each keypair In dictionary
            Select Case keypair.Key.ToString
                Case "interval"
                    'MsgBox TimeSpan.FromSeconds(int.Parse(keypair.Value.ToString())); 
                    'MsgBox(keypair.Value.ToString)
                Case "peers"
                    If TypeOf keypair.Value Is BEncodedList Then
                        peers.AddRange(Peer.Decode(DirectCast(keypair.Value, BEncodedList)))
                    ElseIf TypeOf keypair.Value Is BEncodedString Then
                        peers.AddRange(Peer.Decode(DirectCast(keypair.Value, BEncodedString)))
                    End If
                Case Else
                    'MsgBox("HttpTracker - Unknown announce tag received:" & keypair.Key.ToString() & keypair.Value.ToString())
            End Select
        Next
 
        Return peers
    End Function
 
    ''' <summary>
    ''' Original CreateAnnounceString is private only and we need to modify it a little bit; this the old one out of svn
    ''' they have created here a better function that uses UriQueryBuilder but its oversized here
    ''' 
    ''' MonoTorrent.Client.Tracker.HTTPTracker.CreateAnnounceString
    ''' http://anonsvn.mono-project.com/viewvc/trunk/bitsharp/src/MonoTorrent/MonoTorrent.Client/Tracker/HTTPTracker.cs?revision=141866
    ''' </summary>
    ''' <param name="parameters"></param>
    ''' <param name="Uri">AnnounceURL of the TorrentFile</param>
    ''' <param name="Key">a radon key paramter</param>
    ''' <returns>url to use with a WebRequest</returns>
    ''' <remarks></remarks>
    Private Function CreateAnnounceString(ByVal parameters As AnnounceParameters, ByVal Uri As String, ByVal Key As String) As String
        Dim sb As New System.Text.StringBuilder(256)
 
        'base.LastUpdated = DateTime.Now;
        ' FIXME: This method should be tidied up. I don't like the way it current works
        sb.Append(Uri)
        sb.Append(If(Uri.Contains("?"), "&"c, "?"c))
        sb.Append("info_hash=")
        sb.Append(parameters.InfoHash.UrlEncode())
        sb.Append("&peer_id=")
        sb.Append(parameters.PeerId)
        sb.Append("&port=")
        sb.Append(parameters.Port)
        If parameters.SupportsEncryption Then
            sb.Append("&supportcrypto=1")
        End If
        If parameters.RequireEncryption Then
            sb.Append("&requirecrypto=1")
        End If
        sb.Append("&uploaded=")
        sb.Append(parameters.BytesUploaded)
        sb.Append("&downloaded=")
        sb.Append(parameters.BytesDownloaded)
        sb.Append("&left=")
        sb.Append(parameters.BytesLeft)
        sb.Append("&compact=1")
        ' Always use compact response
        sb.Append("&numwant=")
        sb.Append(100)
        If Not Uri.Contains("&key=") AndAlso Not Uri.Contains("?key=") Then
            sb.Append("&key=")
            ' The 'key' protocol, used as a kind of 'password'. Must be the same between announces
            sb.Append(Key)
        End If
        If parameters.Ipaddress IsNot Nothing Then
            sb.Append("&ip=")
            sb.Append(parameters.Ipaddress)
        End If
 
        ' If we have not successfully sent the started event to this tier, override the passed in started event
        ' Otherwise append the event if it is not "none"
        'if (!parameters.Id.Tracker.Tier.SentStartedEvent)
        '{
        '    sb.Append("&event=started");
        '    parameters.Id.Tracker.Tier.SendingStartedEvent = true;
        '}
        If parameters.ClientEvent <> TorrentEvent.None Then
            sb.Append("&event=")
            sb.Append(parameters.ClientEvent.ToString().ToLower())
        End If
 
 
        Return sb.ToString()
    End Function
    ''' <summary>
    ''' HttpWebResponse and GetResponseStream dont gives use a full readable stream so we must convert it
    ''' 
    ''' Look at: MonoTorrent.Client.Tracker.HTTPTracker.DecodeResponse
    ''' http://anonsvn.mono-project.com/viewvc/trunk/bitsharp/src/MonoTorrent/MonoTorrent.Client/Tracker/HTTPTracker.cs
    ''' or
    ''' http://bytes.com/topic/c-sharp/answers/232436-download-binary-file-http#post949811
    ''' </summary>
    ''' <param name="response"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Function WebResponseToStream(ByVal response As HttpWebResponse) As Stream
 
        Dim responseStream As Stream = response.GetResponseStream
        Dim fs As MemoryStream = New MemoryStream(256)
 
        Dim buffer As Byte() = New Byte(4095) {}
        Dim length As Integer = responseStream.Read(buffer, 0, 4096)
 
        While length > 0
            fs.Write(buffer, 0, length)
            length = responseStream.Read(buffer, 0, 4096)
        End While
 
        fs.Seek(0, SeekOrigin.Begin)
 
        Return fs
    End Function
End Module