Parsing tracker response

This commit is contained in:
Ben Davis
2017-06-28 22:55:06 +01:00
parent 42174d1418
commit e7d3e0af90
7 changed files with 433 additions and 73 deletions
+16
View File
@@ -35,6 +35,8 @@
B5E9B0DA1F02E6F800EF58E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5E9B0D91F02E6F800EF58E3 /* Assets.xcassets */; };
B5E9B0DD1F02E6F800EF58E3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B5E9B0DB1F02E6F800EF58E3 /* LaunchScreen.storyboard */; };
B5E9B0E51F02FAC600EF58E3 /* TestText.torrent in Resources */ = {isa = PBXBuildFile; fileRef = B5E9B0E31F02F9E700EF58E3 /* TestText.torrent */; };
B5F81E491F0436D600B25C70 /* TorrentPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F81E481F0436D600B25C70 /* TorrentPeer.swift */; };
B5F81E4B1F04399800B25C70 /* TorrentTrackerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F81E4A1F04399800B25C70 /* TorrentTrackerResponse.swift */; };
F2A74A776590BB3F37116B02 /* Pods_BitTorrent.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B95A5AC293A32B10DB4660E /* Pods_BitTorrent.framework */; };
/* End PBXBuildFile section */
@@ -145,6 +147,8 @@
B5E9B0DE1F02E6F800EF58E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B5E9B0E31F02F9E700EF58E3 /* TestText.torrent */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestText.torrent; sourceTree = "<group>"; };
B5E9B0E41F02F9E700EF58E3 /* text.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = text.txt; sourceTree = "<group>"; };
B5F81E481F0436D600B25C70 /* TorrentPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentPeer.swift; sourceTree = "<group>"; };
B5F81E4A1F04399800B25C70 /* TorrentTrackerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TorrentTrackerResponse.swift; path = Tracker/TorrentTrackerResponse.swift; sourceTree = "<group>"; };
BDA22A3943D47D6B2152CB15 /* Pods-BitTorrent.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitTorrent.debug.xcconfig"; path = "Pods/Target Support Files/Pods-BitTorrent/Pods-BitTorrent.debug.xcconfig"; sourceTree = "<group>"; };
BFDACB202E071FF8247EC868 /* Pods-BitTorrentExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitTorrentExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-BitTorrentExample/Pods-BitTorrentExample.debug.xcconfig"; sourceTree = "<group>"; };
C79B494173728B25AA5E2B62 /* Pods-BitTorrent-BitTorrentExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitTorrent-BitTorrentExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-BitTorrent-BitTorrentExample/Pods-BitTorrent-BitTorrentExample.release.xcconfig"; sourceTree = "<group>"; };
@@ -269,6 +273,7 @@
isa = PBXGroup;
children = (
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */,
B5F81E4A1F04399800B25C70 /* TorrentTrackerResponse.swift */,
B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */,
);
name = Tracker;
@@ -304,6 +309,7 @@
children = (
B585AB771C3833450093FA41 /* BitTorrent.h */,
B585AB831C3833450093FA41 /* BitTorrentTests.swift */,
B5F81E461F0436CC00B25C70 /* Peer */,
B55317DA1F02FC3000909ADF /* Tracker */,
B54D0C251CA56A25004343BD /* Models */,
B54D0C291CA5785F004343BD /* Utilities */,
@@ -354,6 +360,14 @@
path = "Single text file";
sourceTree = "<group>";
};
B5F81E461F0436CC00B25C70 /* Peer */ = {
isa = PBXGroup;
children = (
B5F81E481F0436D600B25C70 /* TorrentPeer.swift */,
);
path = Peer;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -702,9 +716,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B5F81E491F0436D600B25C70 /* TorrentPeer.swift in Sources */,
B55317DC1F02FC4D00909ADF /* TorrentHTTPTracker.swift in Sources */,
B5E977961CAFB46B0038EBE7 /* String+URLEncode.swift in Sources */,
B54D0C7A1CA69FAD004343BD /* TorrentMetaInfo.swift in Sources */,
B5F81E4B1F04399800B25C70 /* TorrentTrackerResponse.swift in Sources */,
B54D0C7B1CA69FD8004343BD /* Data+sha1.swift in Sources */,
B5530DB21F03063300F71CCD /* HTTPConnection.swift in Sources */,
);
+57
View File
@@ -0,0 +1,57 @@
//
// TorrentPeer.swift
// BitTorrent
//
// Created by Ben Davis on 28/06/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import Foundation
struct TorrentPeerInfo {
let ip: String
let port: Int
let peerId: Data?
init(ip: String, port: Int, peerId: Data?) {
self.ip = ip
self.port = port
self.peerId = peerId
}
init?(dictionary: [String: Any]) {
guard let ipData = dictionary["ip"] as? Data,
let ip = String(asciiData: ipData),
let port = dictionary["port"] as? Int else {
return nil
}
self.ip = ip
self.port = port
self.peerId = dictionary["peer id"] as? Data
}
static func peersInfoFromBinaryModel(_ data: Data) -> [TorrentPeerInfo] {
let numberOfPeers = data.count / 6
var result: [TorrentPeerInfo] = []
for i in 0..<numberOfPeers {
let ip1 = Int(data[i*6])
let ip2 = Int(data[i*6 + 1])
let ip3 = Int(data[i*6 + 2])
let ip4 = Int(data[i*6 + 3])
let portBytes = [data[i*6 + 5], data[i*6 + 4]]
let portu16 = UnsafePointer(portBytes).withMemoryRebound(to: UInt16.self, capacity: 1) {
$0.pointee
}
let port = Int(portu16)
print("Found peer: \(ip1).\(ip2).\(ip3).\(ip4):\(port)")
let peer = TorrentPeerInfo(ip: "\(ip1).\(ip2).\(ip3).\(ip4)", port: port, peerId: nil)
result.append(peer)
}
return result
}
}
+69 -46
View File
@@ -8,12 +8,75 @@
import Foundation
//tracker.announceClient(self.peerId,
// port: 6881,
// numberOfBytes: self.metaInfo.length,
// infoHash: self.metaInfo.infoHash,
// numwant: 20,
// key: "-BD0000-bxa]N#IRKqv`");
protocol TorrentTrackerDelegate: class {
func torrentTracker(_ sender: TorrentHTTPTracker, receivedResponse: TorrentHTTPTrackerResponse)
func torrentTracker(_ sender: TorrentHTTPTracker, receivedErrorMessage: String)
}
enum TorrentHTTPTrackerEvent {
case started, stopped, completed
var name: String {
switch self {
case .started:
return "started"
case .stopped:
return "stopped"
case .completed:
return "completed"
}
}
}
class TorrentHTTPTracker {
let metaInfo: TorrentMetaInfo
let connection: BasicHTTPConnection
weak var delegate: TorrentTrackerDelegate?
init(metaInfo: TorrentMetaInfo, connection: BasicHTTPConnection = HTTPConnection()) {
self.metaInfo = metaInfo
self.connection = connection
}
func announceClient(with peerId: String,
port: Int,
event: TorrentHTTPTrackerEvent = .started,
infoHash: Data,
numberOfBytesRemaining: Int,
numberOfBytesUploaded: Int,
numberOfBytesDownloaded: Int,
numberOfPeersToFetch: Int) {
let urlParameters = [
"info_hash" : String(urlEncodingData: metaInfo.infoHash),
"peer_id" : "\(peerId)",
"port" : "\(port)",
"uploaded" : "\(numberOfBytesUploaded)",
"downloaded" : "\(numberOfBytesDownloaded)",
"left" : "\(numberOfBytesRemaining)",
"compact" : "1",
"event" : event.name,
"numwant" : "\(numberOfPeersToFetch)"
]
connection.makeRequest(url: metaInfo.announce, urlParameters: urlParameters) { [weak self] response in
guard self != nil else {
return
}
if let data = response.responseData {
if let result = TorrentHTTPTrackerResponse(data: data) {
self!.delegate?.torrentTracker(self!, receivedResponse: result)
} else if let errorMessage = TorrentHTTPTrackerResponse.errorMessage(fromResponseData: data) {
self!.delegate?.torrentTracker(self!, receivedErrorMessage: errorMessage)
}
}
}
}
}
//func makePeerId() -> String {
// var peerId = "-BD0000-"
@@ -30,43 +93,3 @@ import Foundation
//
// return peerId
//}
class TorrentHTTPTracker {
let metaInfo: TorrentMetaInfo
let connection: BasicHTTPConnection
init(metaInfo: TorrentMetaInfo, connection: BasicHTTPConnection = HTTPConnection()) {
self.metaInfo = metaInfo
self.connection = connection
}
func announceClient(with peerId: String,
port: Int,
numberOfBytesRemaining: Int,
infoHash: Data,
numberOfPeersToFetch: Int,
peerKey: String) {
let urlParameters = [
"info_hash" : String(urlEncodingData: metaInfo.infoHash),
"peer_id" : "\(peerId)",
"port" : "\(port)",
"uploaded" : "0",
"downloaded" : "0",
"left" : "\(numberOfBytesRemaining)",
"compact" : "1",
"event" : "started",
"numwant" : "\(numberOfPeersToFetch)",
"key" : peerKey,
]
connection.makeRequest(url: metaInfo.announce, urlParameters: urlParameters) { response in
if let data = response.responseData, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)")
}
}
}
}
+185 -22
View File
@@ -9,11 +9,46 @@
import XCTest
@testable import BitTorrent
class TorrentTrackerDelegateSpy: TorrentTrackerDelegate {
var receivedResponseCalled = false
var receivedResponseParameter: TorrentHTTPTrackerResponse? = nil
var receivedErrorMessageCalled = false
var receivedErrorMessageParameter: String? = nil
func torrentTracker(_ sender: TorrentHTTPTracker, receivedResponse response: TorrentHTTPTrackerResponse) {
receivedResponseCalled = true
receivedResponseParameter = response
}
func torrentTracker(_ sender: TorrentHTTPTracker, receivedErrorMessage errorMessage: String) {
receivedErrorMessageCalled = true
receivedErrorMessageParameter = errorMessage
}
}
class TorrentHTTPTrackerTests: XCTestCase {
var connectionStub: HTTPConnectionStub!
var sut: TorrentHTTPTracker!
var delegateSpy: TorrentTrackerDelegateSpy!
let expectedURLParameters: [String: String] = [
"info_hash": "%F0%B8q%98%99S%97%3F%BF%A9M%C8%14%98%EE%8D%20%5B%B2%23",
"peer_id" : "peerId",
"port" : "123",
"uploaded" : "1234",
"downloaded" : "4321",
"left" : "456",
"compact" : "1",
"event" : "started",
"numwant" : "321",
]
let basicResponseData = "d8:completei1e10:incompletei2e8:intervali600e5:peers0:e".data(using: .ascii)!
override func setUp() {
super.setUp()
@@ -23,34 +58,162 @@ class TorrentHTTPTrackerTests: XCTestCase {
connectionStub = HTTPConnectionStub()
sut = TorrentHTTPTracker(metaInfo: metaInfo, connection: connectionStub)
delegateSpy = TorrentTrackerDelegateSpy()
sut.delegate = delegateSpy
}
func performAnnounce(withEvent event: TorrentHTTPTrackerEvent) {
sut.announceClient(with: "peerId",
port: 123,
event: event,
infoHash: Data(bytes: [7,8,9]),
numberOfBytesRemaining: 456,
numberOfBytesUploaded: 1234,
numberOfBytesDownloaded: 4321,
numberOfPeersToFetch: 321)
}
func test_announce() {
sut.announceClient(with: "peerId",
port: 123,
numberOfBytesRemaining: 456,
infoHash: Data(bytes: [7,8,9]),
numberOfPeersToFetch: 321,
peerKey: "key")
performAnnounce(withEvent: .started)
let request = connectionStub.lastRequest
let expectedURLParameters = [
"info_hash": "%F0%B8q%98%99S%97%3F%BF%A9M%C8%14%98%EE%8D%20%5B%B2%23",
"peer_id" : "peerId",
"port" : "123",
"uploaded" : "0",
"downloaded" : "0",
"left" : "456",
"compact" : "1",
"event" : "started",
"numwant" : "321",
"key" : "key",
]
XCTAssertEqual(request.url.absoluteString, "http://127.0.0.1:53420/announce")
XCTAssertEqual(request.urlParameters!, expectedURLParameters)
}
func test_sendStoppedEvent() {
performAnnounce(withEvent: .stopped)
let request = connectionStub.lastRequest
XCTAssertEqual(request.urlParameters!["event"], "stopped")
}
func test_sendCompletedEvent() {
performAnnounce(withEvent: .completed)
let request = connectionStub.lastRequest
XCTAssertEqual(request.urlParameters!["event"], "completed")
}
func test_delegateNotifiedOnTrackerResponse() {
performAnnounce(withEvent: .started)
connectionStub.completeLastRequest(with: HTTPResponse(completed: true,
responseData: basicResponseData,
statusCode: 200))
XCTAssert(delegateSpy.receivedResponseCalled)
}
func test_basicResponseParsing() {
performAnnounce(withEvent: .started)
connectionStub.completeLastRequest(with: HTTPResponse(completed: true,
responseData: basicResponseData,
statusCode: 200))
let response = delegateSpy.receivedResponseParameter!
XCTAssertEqual(response.numberOfPeersComplete, 1)
XCTAssertEqual(response.numberOfPeersIncomplete, 2)
XCTAssertNil(response.trackerId)
XCTAssertEqual(response.interval, 600)
XCTAssertEqual(response.minimumInterval, 0)
XCTAssertNil(response.warning)
}
func test_optionalResponseFieldsParsing() {
performAnnounce(withEvent: .started)
let completeResponse = "d15:warning message7:warning10:tracker id9:trackerId12:min intervali60e8:completei1e10:incompletei2e8:intervali600e5:peers0:e".data(using: .ascii)!
connectionStub.completeLastRequest(with: HTTPResponse(completed: true,
responseData: completeResponse,
statusCode: 200))
let response = delegateSpy.receivedResponseParameter!
XCTAssertEqual(response.trackerId, "trackerId".data(using: .ascii))
XCTAssertEqual(response.minimumInterval, 60)
XCTAssertEqual(response.warning, "warning")
}
func test_binaryPeersFormat() {
performAnnounce(withEvent: .started)
let peersBinary = Data(bytes: [0x7f, 0x00, 0x00, 0x01, 0x3c, 0x17, 0x7f, 0x00, 0x00, 0x01, 0x1a, 0xe1])
let responseData = "d8:completei1e10:incompletei2e8:intervali600e5:peers12:".data(using: .ascii)! +
peersBinary +
"e".data(using: .ascii)!
connectionStub.completeLastRequest(with: HTTPResponse(completed: true,
responseData: responseData,
statusCode: 200))
let response = delegateSpy.receivedResponseParameter!
XCTAssertEqual(response.peers.count, 2)
XCTAssertEqual(response.peers.first!.ip, "127.0.0.1")
XCTAssertEqual(response.peers.first!.port, 15383)
XCTAssertEqual(response.peers.last!.ip, "127.0.0.1")
XCTAssertEqual(response.peers.last!.port, 6881)
}
func test_dictionaryPeersFormat() {
performAnnounce(withEvent: .started)
let peer1Id = "peerId1-------------"
let peer1IP = "127.0.0.1"
let peer1Port = 15383
let peer2Id = "peerId2-------------"
let peer2IP = "127.0.0.1"
let peer2Port = 6881
let peer1 = "d7:peer id20:\(peer1Id)2:ip9:\(peer1IP)4:porti\(peer1Port)ee"
let peer2 = "d7:peer id20:\(peer2Id)2:ip9:\(peer2IP)4:porti\(peer2Port)ee"
let responseData = "d8:completei1e10:incompletei2e8:intervali600e5:peersl\(peer1)\(peer2)ee".data(using: .ascii)!
connectionStub.completeLastRequest(with: HTTPResponse(completed: true,
responseData: responseData,
statusCode: 200))
let response = delegateSpy.receivedResponseParameter!
XCTAssertEqual(response.peers.count, 2)
XCTAssertEqual(response.peers.first!.peerId!, peer1Id.data(using: .ascii))
XCTAssertEqual(response.peers.first!.ip, peer1IP)
XCTAssertEqual(response.peers.first!.port, peer1Port)
XCTAssertEqual(response.peers.last!.peerId!, peer2Id.data(using: .ascii))
XCTAssertEqual(response.peers.last!.ip, peer2IP)
XCTAssertEqual(response.peers.last!.port, peer2Port)
}
func test_failResponseDoesNotCallDelegate() {
performAnnounce(withEvent: .started)
let responseData = "d14:failure reason45:invalid info_hash (not 20 chars):123length: 3e".data(using: .ascii)
connectionStub.completeLastRequest(with: HTTPResponse(completed: true,
responseData: responseData,
statusCode: 200))
XCTAssertFalse(delegateSpy.receivedResponseCalled)
XCTAssert(delegateSpy.receivedErrorMessageCalled)
XCTAssertEqual(delegateSpy.receivedErrorMessageParameter, "invalid info_hash (not 20 chars):123length: 3")
}
func test_invalidTrackerResponse() {
performAnnounce(withEvent: .started)
connectionStub.completeLastRequest(with: HTTPResponse(completed: true,
responseData: Data(),
statusCode: 500))
XCTAssertFalse(delegateSpy.receivedResponseCalled)
XCTAssertFalse(delegateSpy.receivedErrorMessageCalled)
}
}
@@ -0,0 +1,95 @@
//
// TorrentTrackerResponse.swift
// BitTorrent
//
// Created by Ben Davis on 28/06/2017.
// Copyright © 2017 Ben Davis. All rights reserved.
//
import Foundation
import BEncode
struct TorrentHTTPTrackerResponse {
let peers: [TorrentPeerInfo]
let numberOfPeersComplete: Int // Seeders
let numberOfPeersIncomplete: Int // Leechers
let trackerId: Data?
let interval: Int
let minimumInterval: Int
let warning: String?
init(peers: [TorrentPeerInfo],
numberOfPeersComplete: Int = 0,
numberOfPeersIncomplete: Int = 0,
trackerId: Data? = nil,
interval: Int = 60,
minimumInterval: Int = 0,
warning: String? = nil) {
self.peers = peers
self.numberOfPeersComplete = numberOfPeersComplete
self.numberOfPeersIncomplete = numberOfPeersIncomplete
self.trackerId = trackerId
self.interval = interval
self.minimumInterval = minimumInterval
self.warning = warning
}
}
extension TorrentHTTPTrackerResponse {
init?(data: Data) {
let bencode: [String: Any]
do {
bencode = try BEncoder.decodeStringKeyedDictionary(data)
} catch {
return nil
}
guard let numberOfPeersComplete = bencode["complete"] as? Int,
let numberOfPeersIncomplete = bencode["incomplete"] as? Int,
let interval = bencode["interval"] as? Int,
let peersObject = bencode["peers"] else {
return nil
}
if let binaryData = peersObject as? Data {
self.peers = TorrentPeerInfo.peersInfoFromBinaryModel(binaryData)
} else {
guard let peersDictionaries = peersObject as? [[String: Any]] else {
return nil
}
self.peers = peersDictionaries.map(TorrentPeerInfo.init(dictionary:)).flatMap({ $0 })
}
self.numberOfPeersComplete = numberOfPeersComplete
self.numberOfPeersIncomplete = numberOfPeersIncomplete
self.trackerId = bencode["tracker id"] as? Data
self.interval = interval
self.minimumInterval = bencode["min interval"] as? Int ?? 0
if let warningData = bencode["warning message"] as? Data {
self.warning = String(data: warningData, encoding: .utf8)
} else {
self.warning = nil
}
}
}
extension TorrentHTTPTrackerResponse {
static func errorMessage(fromResponseData data: Data) -> String? {
let bencode: [String: Any]
do {
bencode = try BEncoder.decodeStringKeyedDictionary(data)
} catch {
return nil
}
return String(asciiData: bencode["failure reason"] as? Data)
}
}
+6 -5
View File
@@ -31,11 +31,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
tracker.announceClient(with: "-BD0000-bxa]N#IRKqv`",
port: 6881,
numberOfBytesRemaining: 117,
infoHash: Data(bytes:[ 0xf0, 0xb8, 0x71, 0x98, 0x99, 0x53, 0x97, 0x3f, 0xbf, 0xa9,
0x4d, 0xc8, 0x14, 0x98, 0xee, 0x8d, 0x20, 0x5b, 0xb2, 0x23]),
numberOfPeersToFetch: 50,
peerKey: "key")
event: .started,
infoHash: metaInfo.infoHash,
numberOfBytesRemaining: metaInfo.info.length,
numberOfBytesUploaded: 0,
numberOfBytesDownloaded: 0,
numberOfPeersToFetch: 50)
return true
}
@@ -84,6 +84,11 @@
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {