Torrent UDP Tracker
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
867491B4C409A91D4D72CCE7 /* Pods_BitTorrent_BitTorrentExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D97F9FB9F74204574FF6840B /* Pods_BitTorrent_BitTorrentExample.framework */; };
|
||||
B50B24F71F0A553F00C23E7C /* UDPConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50B24F61F0A553B00C23E7C /* UDPConnectionTests.swift */; };
|
||||
B50B24F91F0A554A00C23E7C /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50B24F81F0A554A00C23E7C /* UDPConnection.swift */; };
|
||||
B51D6C091F0C180600E1E3AB /* TorrentUDPTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51D6C031F0C17AE00E1E3AB /* TorrentUDPTrackerTests.swift */; };
|
||||
B51D6C0A1F0C180D00E1E3AB /* TorrentUDPTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51D6C061F0C17C000E1E3AB /* TorrentUDPTracker.swift */; };
|
||||
B537CF061F03148B0084089B /* HTTPConnectionStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = B537CF051F03148B0084089B /* HTTPConnectionStub.swift */; };
|
||||
B537CF461F031AD20084089B /* TestText.torrent in Resources */ = {isa = PBXBuildFile; fileRef = B5E9B0E31F02F9E700EF58E3 /* TestText.torrent */; };
|
||||
B537CF491F031C3D0084089B /* BEncode.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B54D0C1A1CA53983004343BD /* BEncode.framework */; };
|
||||
@@ -34,6 +36,7 @@
|
||||
B585AB781C3833450093FA41 /* BitTorrent.h in Headers */ = {isa = PBXBuildFile; fileRef = B585AB771C3833450093FA41 /* BitTorrent.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
B585AB7F1C3833450093FA41 /* BitTorrent.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B585AB741C3833450093FA41 /* BitTorrent.framework */; };
|
||||
B585AB841C3833450093FA41 /* BitTorrentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B585AB831C3833450093FA41 /* BitTorrentTests.swift */; };
|
||||
B59E1B281F0E6EA3007753CE /* BitTorrentTestMacros.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59E1B261F0E6E5F007753CE /* BitTorrentTestMacros.swift */; };
|
||||
B5BD7FD61F03032400621BC2 /* TorrentHTTPTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */; };
|
||||
B5E977961CAFB46B0038EBE7 /* String+URLEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E977951CAFB46B0038EBE7 /* String+URLEncode.swift */; };
|
||||
B5E9B0D81F02E6F800EF58E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E9B0D71F02E6F800EF58E3 /* AppDelegate.swift */; };
|
||||
@@ -119,6 +122,8 @@
|
||||
A8680B0EA21B5A20E03DEE6D /* Pods_BitTorrentTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BitTorrentTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B50B24F61F0A553B00C23E7C /* UDPConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnectionTests.swift; sourceTree = "<group>"; };
|
||||
B50B24F81F0A554A00C23E7C /* UDPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPConnection.swift; sourceTree = "<group>"; };
|
||||
B51D6C031F0C17AE00E1E3AB /* TorrentUDPTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentUDPTrackerTests.swift; sourceTree = "<group>"; };
|
||||
B51D6C061F0C17C000E1E3AB /* TorrentUDPTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentUDPTracker.swift; sourceTree = "<group>"; };
|
||||
B537CF051F03148B0084089B /* HTTPConnectionStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HTTPConnectionStub.swift; path = "HTTP Networking/HTTPConnectionStub.swift"; sourceTree = "<group>"; };
|
||||
B54D0C141CA53983004343BD /* BEncode.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = BEncode.xcodeproj; path = Submodules/BEncodeSwift/BEncode.xcodeproj; sourceTree = "<group>"; };
|
||||
B54D0C231CA56A22004343BD /* TorrentMetaInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TorrentMetaInfo.swift; path = Models/TorrentMetaInfo.swift; sourceTree = "<group>"; };
|
||||
@@ -133,7 +138,7 @@
|
||||
B551C9541F0B9D3D004115CB /* GCDAsyncUdpSocketStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GCDAsyncUdpSocketStub.swift; sourceTree = "<group>"; };
|
||||
B5530DB11F03063300F71CCD /* HTTPConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HTTPConnection.swift; path = "HTTP Networking/HTTPConnection.swift"; sourceTree = "<group>"; };
|
||||
B5530DB41F03063E00F71CCD /* HTTPConnectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HTTPConnectionTests.swift; path = "HTTP Networking/HTTPConnectionTests.swift"; sourceTree = "<group>"; };
|
||||
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TorrentHTTPTracker.swift; path = Tracker/TorrentHTTPTracker.swift; sourceTree = "<group>"; };
|
||||
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentHTTPTracker.swift; sourceTree = "<group>"; };
|
||||
B55317DF1F02FE1500909ADF /* URLEncodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLEncodeTests.swift; path = "HTTP Networking/URLEncodeTests.swift"; sourceTree = "<group>"; };
|
||||
B558F4821F0A647D00438BB4 /* InternetProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetProtocol.swift; sourceTree = "<group>"; };
|
||||
B558F4841F0A73D000438BB4 /* InternetProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetProtocolTests.swift; sourceTree = "<group>"; };
|
||||
@@ -144,7 +149,8 @@
|
||||
B585AB7E1C3833450093FA41 /* BitTorrentTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BitTorrentTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B585AB831C3833450093FA41 /* BitTorrentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BitTorrentTests.swift; path = BitTorrentTests/BitTorrentTests.swift; sourceTree = SOURCE_ROOT; };
|
||||
B585AB851C3833450093FA41 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TorrentHTTPTrackerTests.swift; path = Tracker/TorrentHTTPTrackerTests.swift; sourceTree = "<group>"; };
|
||||
B59E1B261F0E6E5F007753CE /* BitTorrentTestMacros.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BitTorrentTestMacros.swift; path = Utilities/BitTorrentTestMacros.swift; sourceTree = "<group>"; };
|
||||
B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentHTTPTrackerTests.swift; sourceTree = "<group>"; };
|
||||
B5E977951CAFB46B0038EBE7 /* String+URLEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "String+URLEncode.swift"; path = "HTTP Networking/String+URLEncode.swift"; sourceTree = "<group>"; };
|
||||
B5E9B0D51F02E6F800EF58E3 /* BitTorrentExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BitTorrentExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B5E9B0D71F02E6F800EF58E3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -154,7 +160,7 @@
|
||||
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>"; };
|
||||
B5F81E4A1F04399800B25C70 /* TorrentTrackerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentTrackerResponse.swift; sourceTree = "<group>"; };
|
||||
D437CE230D6B283EED6C1A3E /* 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>"; };
|
||||
D97F9FB9F74204574FF6840B /* Pods_BitTorrent_BitTorrentExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BitTorrent_BitTorrentExample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EDC27D7221FFFF5E25D0A77F /* Pods-BitTorrent-BitTorrentExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitTorrent-BitTorrentExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-BitTorrent-BitTorrentExample/Pods-BitTorrent-BitTorrentExample.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -286,11 +292,13 @@
|
||||
B55317DA1F02FC3000909ADF /* Tracker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */,
|
||||
B5F81E4A1F04399800B25C70 /* TorrentTrackerResponse.swift */,
|
||||
B55317DB1F02FC4D00909ADF /* TorrentHTTPTracker.swift */,
|
||||
B5BD7FD51F03032400621BC2 /* TorrentHTTPTrackerTests.swift */,
|
||||
B51D6C061F0C17C000E1E3AB /* TorrentUDPTracker.swift */,
|
||||
B51D6C031F0C17AE00E1E3AB /* TorrentUDPTrackerTests.swift */,
|
||||
);
|
||||
name = Tracker;
|
||||
path = Tracker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B585AB6A1C3833450093FA41 = {
|
||||
@@ -339,6 +347,7 @@
|
||||
children = (
|
||||
B585AB851C3833450093FA41 /* Info.plist */,
|
||||
B56A8A061C83539300426AC8 /* TestHelpers.swift */,
|
||||
B59E1B261F0E6E5F007753CE /* BitTorrentTestMacros.swift */,
|
||||
);
|
||||
path = BitTorrentTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -741,6 +750,7 @@
|
||||
B54D0C7B1CA69FD8004343BD /* Data+sha1.swift in Sources */,
|
||||
B5530DB21F03063300F71CCD /* HTTPConnection.swift in Sources */,
|
||||
B50B24F91F0A554A00C23E7C /* UDPConnection.swift in Sources */,
|
||||
B51D6C0A1F0C180D00E1E3AB /* TorrentUDPTracker.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -748,6 +758,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B51D6C091F0C180600E1E3AB /* TorrentUDPTrackerTests.swift in Sources */,
|
||||
B537CF061F03148B0084089B /* HTTPConnectionStub.swift in Sources */,
|
||||
B5BD7FD61F03032400621BC2 /* TorrentHTTPTrackerTests.swift in Sources */,
|
||||
B556D5AE1F0BA1FD00277B8D /* GCDAsyncUdpSocketStub.swift in Sources */,
|
||||
@@ -758,6 +769,7 @@
|
||||
B55317E01F02FE1500909ADF /* URLEncodeTests.swift in Sources */,
|
||||
B558F4851F0A73D000438BB4 /* InternetProtocolTests.swift in Sources */,
|
||||
B50B24F71F0A553F00C23E7C /* UDPConnectionTests.swift in Sources */,
|
||||
B59E1B281F0E6EA3007753CE /* BitTorrentTestMacros.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -46,9 +46,7 @@ struct TorrentPeerInfo {
|
||||
$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)
|
||||
}
|
||||
|
||||
@@ -8,26 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
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
|
||||
@@ -42,7 +22,7 @@ class TorrentHTTPTracker {
|
||||
|
||||
func announceClient(with peerId: String,
|
||||
port: Int,
|
||||
event: TorrentHTTPTrackerEvent = .started,
|
||||
event: TorrentTrackerEvent = .started,
|
||||
infoHash: Data,
|
||||
numberOfBytesRemaining: Int,
|
||||
numberOfBytesUploaded: Int,
|
||||
@@ -68,9 +48,9 @@ class TorrentHTTPTracker {
|
||||
}
|
||||
|
||||
if let data = response.responseData {
|
||||
if let result = TorrentHTTPTrackerResponse(data: data) {
|
||||
if let result = TorrentTrackerResponse(bencode: data) {
|
||||
self!.delegate?.torrentTracker(self!, receivedResponse: result)
|
||||
} else if let errorMessage = TorrentHTTPTrackerResponse.errorMessage(fromResponseData: data) {
|
||||
} else if let errorMessage = TorrentTrackerResponse.errorMessage(fromResponseData: data) {
|
||||
self!.delegate?.torrentTracker(self!, receivedErrorMessage: errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,17 +12,17 @@ import XCTest
|
||||
class TorrentTrackerDelegateSpy: TorrentTrackerDelegate {
|
||||
|
||||
var receivedResponseCalled = false
|
||||
var receivedResponseParameter: TorrentHTTPTrackerResponse? = nil
|
||||
var receivedResponseParameter: TorrentTrackerResponse? = nil
|
||||
|
||||
var receivedErrorMessageCalled = false
|
||||
var receivedErrorMessageParameter: String? = nil
|
||||
|
||||
func torrentTracker(_ sender: TorrentHTTPTracker, receivedResponse response: TorrentHTTPTrackerResponse) {
|
||||
func torrentTracker(_ sender: Any, receivedResponse response: TorrentTrackerResponse) {
|
||||
receivedResponseCalled = true
|
||||
receivedResponseParameter = response
|
||||
}
|
||||
|
||||
func torrentTracker(_ sender: TorrentHTTPTracker, receivedErrorMessage errorMessage: String) {
|
||||
func torrentTracker(_ sender: Any, receivedErrorMessage errorMessage: String) {
|
||||
receivedErrorMessageCalled = true
|
||||
receivedErrorMessageParameter = errorMessage
|
||||
}
|
||||
@@ -63,7 +63,7 @@ class TorrentHTTPTrackerTests: XCTestCase {
|
||||
sut.delegate = delegateSpy
|
||||
}
|
||||
|
||||
func performAnnounce(withEvent event: TorrentHTTPTrackerEvent) {
|
||||
func performAnnounce(withEvent event: TorrentTrackerEvent) {
|
||||
sut.announceClient(with: "peerId",
|
||||
port: 123,
|
||||
event: event,
|
||||
|
||||
@@ -9,7 +9,42 @@
|
||||
import Foundation
|
||||
import BEncode
|
||||
|
||||
struct TorrentHTTPTrackerResponse {
|
||||
protocol TorrentTrackerDelegate: class {
|
||||
func torrentTracker(_ sender: Any, receivedResponse response: TorrentTrackerResponse)
|
||||
func torrentTracker(_ sender: Any, receivedErrorMessage errorMessage: String)
|
||||
}
|
||||
|
||||
enum TorrentTrackerEvent {
|
||||
case none, started, stopped, completed
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .none:
|
||||
return "none"
|
||||
case .started:
|
||||
return "started"
|
||||
case .stopped:
|
||||
return "stopped"
|
||||
case .completed:
|
||||
return "completed"
|
||||
}
|
||||
}
|
||||
|
||||
var udpDataRepresentation: Data {
|
||||
switch self {
|
||||
case .none:
|
||||
return UInt32(0).toData()
|
||||
case .started:
|
||||
return UInt32(2).toData()
|
||||
case .stopped:
|
||||
return UInt32(3).toData()
|
||||
case .completed:
|
||||
return UInt32(1).toData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TorrentTrackerResponse {
|
||||
|
||||
let peers: [TorrentPeerInfo]
|
||||
let numberOfPeersComplete: Int // Seeders
|
||||
@@ -40,9 +75,9 @@ struct TorrentHTTPTrackerResponse {
|
||||
}
|
||||
}
|
||||
|
||||
extension TorrentHTTPTrackerResponse {
|
||||
extension TorrentTrackerResponse {
|
||||
|
||||
init?(data: Data) {
|
||||
init?(bencode data: Data) {
|
||||
let bencode: [String: Any]
|
||||
do {
|
||||
bencode = try BEncoder.decodeStringKeyedDictionary(data)
|
||||
@@ -79,7 +114,7 @@ extension TorrentHTTPTrackerResponse {
|
||||
}
|
||||
}
|
||||
|
||||
extension TorrentHTTPTrackerResponse {
|
||||
extension TorrentTrackerResponse {
|
||||
|
||||
static func errorMessage(fromResponseData data: Data) -> String? {
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
//
|
||||
// TorrentUDPTracker.swift
|
||||
// BitTorrent
|
||||
//
|
||||
// Created by Ben Davis on 04/07/2017.
|
||||
// Copyright © 2017 Ben Davis. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import BEncode
|
||||
|
||||
private let PROTOCOL_ID = UInt64(0x41727101980).toData() // magic constant (protocol_id)
|
||||
|
||||
private let CONNECTION_ACTION = UInt32(0).toData()
|
||||
private let ANNOUNCE_ACTION = UInt32(1).toData()
|
||||
private let ERROR_ACTION = UInt32(3).toData()
|
||||
|
||||
class TorrentUDPTracker {
|
||||
|
||||
var enableLogging = false
|
||||
|
||||
weak var delegate: TorrentTrackerDelegate?
|
||||
|
||||
private let announceURL: URL
|
||||
private var discoveredHostIpAddress: String?
|
||||
private let udpConnection: UDPConnectionProtocol
|
||||
|
||||
private var pendingAnnounce: ((_ transactionId: Data, _ connectionId: Data)->Void)?
|
||||
private var pendingTransactionId: Data?
|
||||
|
||||
init(announceURL: URL, port: UInt16, udpConnection: UDPConnectionProtocol = UDPConnection()) {
|
||||
self.announceURL = announceURL
|
||||
self.udpConnection = udpConnection
|
||||
udpConnection.delegate = self
|
||||
udpConnection.startListening(on: port)
|
||||
}
|
||||
|
||||
func announceClient(with peerId: String,
|
||||
port: Int,
|
||||
event: TorrentTrackerEvent = .started,
|
||||
infoHash: Data,
|
||||
numberOfBytesRemaining: Int,
|
||||
numberOfBytesUploaded: Int,
|
||||
numberOfBytesDownloaded: Int,
|
||||
numberOfPeersToFetch: Int) {
|
||||
|
||||
guard let host = getHostIpAddress() else { return }
|
||||
let announcePort = UInt16(announceURL.port ?? 80)
|
||||
|
||||
log("Will connect to UDP tracker: \(host):\(announcePort)")
|
||||
|
||||
let transactionId = makeTransactionId()
|
||||
let payload = makeConnectionPayload(with: transactionId)
|
||||
udpConnection.send(payload, toHost: host, port: announcePort, timeout: 10)
|
||||
|
||||
pendingAnnounce = { [weak self] (responseTransactionId, connectionId) in
|
||||
|
||||
guard let strongSelf = self else { return }
|
||||
guard responseTransactionId == transactionId else { return }
|
||||
|
||||
self?.log("Will announce to UDP tracker: \(host):\(announcePort)")
|
||||
|
||||
var payload = connectionId // 0 64-bit integer connection_id
|
||||
payload += ANNOUNCE_ACTION // 8 32-bit integer action 1 // announce
|
||||
payload += strongSelf.makeTransactionId() // 12 32-bit integer transaction_id
|
||||
payload += infoHash // 16 20-byte string info_hash
|
||||
payload += peerId.data(using: .ascii)! // 36 20-byte string peer_id
|
||||
payload += UInt64(numberOfBytesDownloaded).toData() // 56 64-bit integer downloaded
|
||||
payload += UInt64(numberOfBytesRemaining).toData() // 64 64-bit integer left
|
||||
payload += UInt64(numberOfBytesUploaded).toData() // 72 64-bit integer uploaded
|
||||
payload += event.udpDataRepresentation // 80 32-bit integer event
|
||||
payload += UInt32(0).toData() // 84 32-bit integer IP address 0 // default
|
||||
payload += UInt32(0).toData() // 88 32-bit integer key 0 // default
|
||||
payload += UInt32(numberOfPeersToFetch).toData() // 92 32-bit integer num_want -1 // default
|
||||
payload += UInt16(port).toData() // 96 16-bit integer port
|
||||
|
||||
strongSelf.udpConnection.send(payload, toHost: host, port: announcePort, timeout: 10)
|
||||
}
|
||||
}
|
||||
|
||||
private func getHostIpAddress() -> String? {
|
||||
guard discoveredHostIpAddress == nil else {
|
||||
return discoveredHostIpAddress
|
||||
}
|
||||
|
||||
let result = InternetProtocol.getIPAddress(of: announceURL.host!)
|
||||
discoveredHostIpAddress = result
|
||||
return result
|
||||
}
|
||||
|
||||
private func makeConnectionPayload(with transactionId: Data) -> Data {
|
||||
return PROTOCOL_ID + CONNECTION_ACTION + transactionId
|
||||
}
|
||||
|
||||
private func makeTransactionId() -> Data {
|
||||
let result = arc4random().toData()
|
||||
pendingTransactionId = result
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension TorrentUDPTracker: UDPConnectionDelegate {
|
||||
|
||||
func udpConnection(_ sender: UDPConnectionProtocol, receivedData data: Data, fromHost host: String) {
|
||||
|
||||
let action = Data(data[0..<4])
|
||||
|
||||
log("Got response from UDP tracker \(host)")
|
||||
|
||||
if action == CONNECTION_ACTION {
|
||||
log("UDP tracker \(host) accepted connection")
|
||||
parseConnectionResponse(data)
|
||||
} else if action == ANNOUNCE_ACTION {
|
||||
log("UDP tracker \(host) responded to announce")
|
||||
parseAnnounceResponse(data)
|
||||
} else if action == ERROR_ACTION {
|
||||
log("UDP tracker \(host) gave error")
|
||||
parseErrorResponse(data)
|
||||
}
|
||||
}
|
||||
|
||||
func parseConnectionResponse(_ response: Data) {
|
||||
|
||||
let transactionId = Data(response[4..<8])
|
||||
let connectionId = Data(response[8..<16])
|
||||
|
||||
pendingAnnounce?(transactionId, connectionId)
|
||||
pendingAnnounce = nil
|
||||
}
|
||||
|
||||
private func parseAnnounceResponse(_ response: Data) {
|
||||
|
||||
let transactionId = Data(response[4..<8])
|
||||
guard pendingTransactionId == transactionId else { return }
|
||||
|
||||
let interval = response[8..<12].toUInt32()
|
||||
let leechers = response[12..<16].toUInt32()
|
||||
let seeders = response[16..<20].toUInt32()
|
||||
let peers = TorrentPeerInfo.peersInfoFromBinaryModel(response[20..<response.count])
|
||||
|
||||
let response = TorrentTrackerResponse(peers: peers,
|
||||
numberOfPeersComplete: Int(seeders),
|
||||
numberOfPeersIncomplete: Int(leechers),
|
||||
interval: Int(interval))
|
||||
|
||||
self.delegate?.torrentTracker(self, receivedResponse: response)
|
||||
}
|
||||
|
||||
private func parseErrorResponse(_ response: Data) {
|
||||
|
||||
if let errorMessage = String(data: response[8..<response.count], encoding: .utf8) {
|
||||
delegate?.torrentTracker(self, receivedErrorMessage: errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TorrentUDPTracker {
|
||||
func log(_ items: Any...) {
|
||||
if enableLogging { print(items) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
//
|
||||
// TorrentUDPTrackerTests.swift
|
||||
// BitTorrent
|
||||
//
|
||||
// Created by Ben Davis on 04/07/2017.
|
||||
// Copyright © 2017 Ben Davis. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import BitTorrent
|
||||
import BEncode
|
||||
|
||||
class UDPConnectionStub: UDPConnectionProtocol {
|
||||
|
||||
weak var delegate: UDPConnectionDelegate?
|
||||
|
||||
var startListeningCalled = false
|
||||
var startListeningParameter: UInt16?
|
||||
func startListening(on port: UInt16) {
|
||||
startListeningCalled = true
|
||||
startListeningParameter = port
|
||||
}
|
||||
|
||||
var sendCallCount = 0
|
||||
var sendDataParameters: (data: Data, host: String, port: UInt16, timeout: TimeInterval)?
|
||||
func send(_ data: Data, toHost host: String, port: UInt16, timeout: TimeInterval) {
|
||||
sendCallCount += 1
|
||||
sendDataParameters = (data, host, port, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
class TorrentUDPTrackerTests: XCTestCase {
|
||||
|
||||
var sut: TorrentUDPTracker!
|
||||
var torrentTrackerDelegateSpy: TorrentTrackerDelegateSpy!
|
||||
var udpConnection: UDPConnectionStub!
|
||||
let port: UInt16 = 123
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let url = URL(string: "udp://localhost:123/announce")!
|
||||
udpConnection = UDPConnectionStub()
|
||||
torrentTrackerDelegateSpy = TorrentTrackerDelegateSpy()
|
||||
sut = TorrentUDPTracker(announceURL: url, port: 123, udpConnection: udpConnection)
|
||||
sut.delegate = torrentTrackerDelegateSpy
|
||||
}
|
||||
|
||||
func performAnnounce(withEvent event: TorrentTrackerEvent) {
|
||||
sut.announceClient(with: "peerId12345678901234",
|
||||
port: 789,
|
||||
event: event,
|
||||
infoHash: Data(bytes: [ 1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0 ]),
|
||||
numberOfBytesRemaining: 456,
|
||||
numberOfBytesUploaded: 1234,
|
||||
numberOfBytesDownloaded: 4321,
|
||||
numberOfPeersToFetch: 321)
|
||||
}
|
||||
|
||||
func test_startsListeningOnPort() {
|
||||
XCTAssert(udpConnection.startListeningCalled)
|
||||
XCTAssertEqual(udpConnection.startListeningParameter, port)
|
||||
}
|
||||
|
||||
func test_hostIsResolvedFromURL() {
|
||||
|
||||
performAnnounce(withEvent: .started)
|
||||
|
||||
XCTAssertEqual(udpConnection.sendCallCount, 1)
|
||||
|
||||
if let parameters = udpConnection.sendDataParameters {
|
||||
XCTAssertEqual(parameters.host, "127.0.0.1")
|
||||
XCTAssertEqual(parameters.port, 123)
|
||||
}
|
||||
}
|
||||
|
||||
func test_connectMessageSentToHost() {
|
||||
|
||||
performAnnounce(withEvent: .started)
|
||||
|
||||
let expectedProtocolId = UInt64(0x41727101980).toData()
|
||||
let expectedAction = UInt32(0).toData()
|
||||
|
||||
XCTAssertEqual(udpConnection.sendCallCount, 1)
|
||||
|
||||
if let parameters = udpConnection.sendDataParameters {
|
||||
|
||||
XCTAssertEqual(parameters.data.count, 16)
|
||||
|
||||
let protocolId = Data(parameters.data[0..<8])
|
||||
XCTAssertEqual(protocolId, expectedProtocolId)
|
||||
|
||||
let action = Data(parameters.data[8..<12])
|
||||
XCTAssertEqual(action, expectedAction)
|
||||
|
||||
XCTAssertEqual(parameters.host, "127.0.0.1")
|
||||
XCTAssertEqual(parameters.port, 123)
|
||||
}
|
||||
}
|
||||
|
||||
func test_announceSentOnConnectionAccepted() {
|
||||
|
||||
// Given
|
||||
performAnnounce(withEvent: .started)
|
||||
|
||||
// When
|
||||
let expectedAction = UInt32(1).toData()
|
||||
let expectedConnectionId = simulateAcceptConnection()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(udpConnection.sendCallCount, 2)
|
||||
if let parameters = udpConnection.sendDataParameters {
|
||||
|
||||
let connectionId = Data(parameters.data[0..<8])
|
||||
XCTAssertEqual(connectionId, expectedConnectionId)
|
||||
|
||||
let action = Data(parameters.data[8..<12])
|
||||
XCTAssertEqual(action, expectedAction)
|
||||
}
|
||||
}
|
||||
|
||||
func test_announcePayload() {
|
||||
|
||||
let peerId = "peerId12345678901234"
|
||||
let exampleEvent = TorrentTrackerEvent.started
|
||||
let examplePort = 789
|
||||
let expectedInfoHash = Data(bytes: [ 1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0 ])
|
||||
let numberOfBytesRemaining = 456
|
||||
let numberOfBytesUploaded = 1234
|
||||
let numberOfBytesDownloaded = 4321
|
||||
let numberOfPeersToFetch = 321
|
||||
|
||||
// Given
|
||||
sut.announceClient(with: peerId,
|
||||
port: examplePort,
|
||||
event: exampleEvent,
|
||||
infoHash: expectedInfoHash,
|
||||
numberOfBytesRemaining: numberOfBytesRemaining,
|
||||
numberOfBytesUploaded: numberOfBytesUploaded,
|
||||
numberOfBytesDownloaded: numberOfBytesDownloaded,
|
||||
numberOfPeersToFetch: numberOfPeersToFetch)
|
||||
// When
|
||||
let expectedConnectionId = simulateAcceptConnection()
|
||||
let expectedAction = UInt32(1).toData()
|
||||
let expectedPeerId = peerId.data(using: .ascii)!
|
||||
let expectedDownloaded = UInt64(numberOfBytesDownloaded).toData()
|
||||
let expectedLeft = UInt64(numberOfBytesRemaining).toData()
|
||||
let expectedUploaded = UInt64(numberOfBytesUploaded).toData()
|
||||
let expectedEvent = exampleEvent.udpDataRepresentation
|
||||
let expectedIPAddress = UInt32(0).toData() // default value
|
||||
let expectedNumWant = UInt32(numberOfPeersToFetch).toData()
|
||||
let expectedPort = UInt16(examplePort).toData()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(udpConnection.sendCallCount, 2)
|
||||
if let data = udpConnection.sendDataParameters?.data {
|
||||
|
||||
XCTAssertEqual(data.count, 98)
|
||||
|
||||
let connectionId = Data(data[0..<8])
|
||||
let action = Data(data[8..<12])
|
||||
// let transactionId = Data(data[12..<16])
|
||||
let infoHash = Data(data[16..<36])
|
||||
let peerId = Data(data[36..<56])
|
||||
let downloaded = Data(data[56..<64])
|
||||
let left = Data(data[64..<72])
|
||||
let uploaded = Data(data[72..<80])
|
||||
let event = Data(data[80..<84])
|
||||
let ipAddress = Data(data[84..<88])
|
||||
// let key = Data(data[88..<92])
|
||||
let numWant = Data(data[92..<96])
|
||||
let port = Data(data[96..<98])
|
||||
|
||||
XCTAssertEqual(connectionId, expectedConnectionId)
|
||||
XCTAssertEqual(action, expectedAction)
|
||||
XCTAssertEqual(infoHash, expectedInfoHash)
|
||||
XCTAssertEqual(peerId, expectedPeerId)
|
||||
XCTAssertEqual(downloaded, expectedDownloaded)
|
||||
XCTAssertEqual(left, expectedLeft)
|
||||
XCTAssertEqual(uploaded, expectedUploaded)
|
||||
XCTAssertEqual(event, expectedEvent)
|
||||
XCTAssertEqual(ipAddress, expectedIPAddress)
|
||||
XCTAssertEqual(numWant, expectedNumWant)
|
||||
XCTAssertEqual(port, expectedPort)
|
||||
}
|
||||
}
|
||||
|
||||
func test_basicResponseParsing() {
|
||||
|
||||
performAnnounce(withEvent: .started)
|
||||
_ = simulateAcceptConnection()
|
||||
|
||||
let interval = 1
|
||||
let seeders = 2
|
||||
let leechers = 3
|
||||
|
||||
simulateAnnounceResponse(interval: interval,
|
||||
leechers: leechers,
|
||||
seeders: seeders,
|
||||
peers: Data())
|
||||
|
||||
XCTAssert(torrentTrackerDelegateSpy.receivedResponseCalled)
|
||||
guard let response = torrentTrackerDelegateSpy.receivedResponseParameter else { return }
|
||||
|
||||
XCTAssertEqual(response.interval, 1)
|
||||
XCTAssertEqual(response.numberOfPeersComplete, seeders)
|
||||
XCTAssertEqual(response.numberOfPeersIncomplete, leechers)
|
||||
XCTAssertEqual(response.peers, [])
|
||||
}
|
||||
|
||||
func test_parsingPeers() {
|
||||
|
||||
performAnnounce(withEvent: .started)
|
||||
_ = simulateAcceptConnection()
|
||||
|
||||
let peers = examplePeersResponse(with: [
|
||||
(127,0,0,1, 15383),
|
||||
(216,58,198,14, 4321),
|
||||
])
|
||||
|
||||
simulateAnnounceResponse(interval: 1,
|
||||
leechers: 2,
|
||||
seeders: 3,
|
||||
peers: peers)
|
||||
|
||||
XCTAssert(torrentTrackerDelegateSpy.receivedResponseCalled)
|
||||
guard let response = torrentTrackerDelegateSpy.receivedResponseParameter else { return }
|
||||
|
||||
XCTAssertEqual(response.peers.count, 2)
|
||||
|
||||
XCTAssertEqual(response.peers.first!.ip, "127.0.0.1")
|
||||
XCTAssertEqual(response.peers.first!.port, 15383)
|
||||
XCTAssertNil(response.peers.first!.peerId)
|
||||
|
||||
XCTAssertEqual(response.peers.last!.ip, "216.58.198.14")
|
||||
XCTAssertEqual(response.peers.last!.port, 4321)
|
||||
XCTAssertNil(response.peers.last!.peerId)
|
||||
}
|
||||
|
||||
func examplePeersResponse(with peers: [(UInt8, UInt8, UInt8, UInt8, UInt16)]) -> Data {
|
||||
|
||||
var result = Data()
|
||||
for peer in peers {
|
||||
|
||||
let ip = Data(bytes: [peer.0, peer.1, peer.2, peer.3])
|
||||
result.append(ip)
|
||||
|
||||
let port = peer.4.toData()
|
||||
result.append(port)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func test_announceMessageForOldTransactionIdIsIgnored() {
|
||||
|
||||
// Given
|
||||
performAnnounce(withEvent: .started)
|
||||
_ = simulateAcceptConnection()
|
||||
guard let connectionParameters = udpConnection.sendDataParameters else { return }
|
||||
let oldTransactionId = connectionParameters.data[4..<8]
|
||||
|
||||
// When
|
||||
performAnnounce(withEvent: .started)
|
||||
_ = simulateAcceptConnection()
|
||||
simulateAnnounceResponse(interval: 1, leechers: 2, seeders: 3, peers: Data(), transactionId: oldTransactionId)
|
||||
|
||||
// Then
|
||||
XCTAssertFalse(torrentTrackerDelegateSpy.receivedResponseCalled)
|
||||
}
|
||||
|
||||
func test_connectionMessageForOldTransactionIdIsIgnored() {
|
||||
|
||||
// Given
|
||||
performAnnounce(withEvent: .started)
|
||||
guard let connectionParameters = udpConnection.sendDataParameters else { return }
|
||||
let oldTransactionId = connectionParameters.data[4..<8]
|
||||
|
||||
// When
|
||||
performAnnounce(withEvent: .started)
|
||||
|
||||
let connectionId = arc4random().toData() + arc4random().toData()
|
||||
let actionData = UInt32(0).toData() // Action 0 = connection
|
||||
let connectionResponse = actionData + oldTransactionId + connectionId
|
||||
|
||||
udpConnection.delegate?.udpConnection(udpConnection,
|
||||
receivedData: connectionResponse,
|
||||
fromHost: "127.0.0.1")
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(udpConnection.sendCallCount, 2)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func simulateAnnounceResponse(interval: Int,
|
||||
leechers: Int,
|
||||
seeders: Int,
|
||||
peers: Data) {
|
||||
|
||||
guard let announceParameters = udpConnection.sendDataParameters else { return }
|
||||
let transactionId = announceParameters.data[12..<16]
|
||||
|
||||
simulateAnnounceResponse(interval: interval,
|
||||
leechers: leechers,
|
||||
seeders: seeders,
|
||||
peers: peers,
|
||||
transactionId: transactionId)
|
||||
}
|
||||
|
||||
func simulateAnnounceResponse(interval: Int,
|
||||
leechers: Int,
|
||||
seeders: Int,
|
||||
peers: Data,
|
||||
transactionId: Data) {
|
||||
|
||||
var announceResponse = UInt32(1).toData()
|
||||
announceResponse += transactionId
|
||||
announceResponse += UInt32(interval).toData()
|
||||
announceResponse += UInt32(leechers).toData()
|
||||
announceResponse += UInt32(seeders).toData()
|
||||
announceResponse += peers
|
||||
|
||||
udpConnection.delegate?.udpConnection(udpConnection,
|
||||
receivedData: announceResponse,
|
||||
fromHost: "127.0.0.1")
|
||||
}
|
||||
|
||||
func simulateAcceptConnection() -> Data {
|
||||
|
||||
let connectionId = arc4random().toData() + arc4random().toData()
|
||||
|
||||
guard let connectionParameters = udpConnection.sendDataParameters else {
|
||||
return connectionId
|
||||
}
|
||||
|
||||
let transactionId = connectionParameters.data[12..<16]
|
||||
let actionData = UInt32(0).toData() // Action 0 = connection
|
||||
let connectionResponse = actionData + transactionId + connectionId
|
||||
|
||||
udpConnection.delegate?.udpConnection(udpConnection,
|
||||
receivedData: connectionResponse,
|
||||
fromHost: "127.0.0.1")
|
||||
|
||||
return connectionId
|
||||
}
|
||||
|
||||
// MARK: - Error handling
|
||||
|
||||
func delegateCalledOnError() {
|
||||
simulateErrorResponse(withError: "Error Message")
|
||||
|
||||
XCTAssert(torrentTrackerDelegateSpy.receivedErrorMessageCalled)
|
||||
XCTAssertEqual(torrentTrackerDelegateSpy.receivedErrorMessageParameter!, "Error Message")
|
||||
}
|
||||
|
||||
func simulateErrorResponse(withError errorString: String) {
|
||||
|
||||
guard let connectionParameters = udpConnection.sendDataParameters else { return }
|
||||
let transactionId = connectionParameters.data[4..<8]
|
||||
|
||||
let connectionResponse = UInt32(3).toData() + // Action 3 = error
|
||||
transactionId + // Responding to transaction
|
||||
errorString.data(using: .utf8)! // Error message
|
||||
|
||||
udpConnection.delegate?.udpConnection(udpConnection,
|
||||
receivedData: connectionResponse,
|
||||
fromHost: "127.0.0.1")
|
||||
}
|
||||
}
|
||||
@@ -8,112 +8,94 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
let IOS_CELLULAR_INTERFACE_NAME = "pdp_ip0"
|
||||
let IOS_WIFI_INTERFACE_NAME = "en0"
|
||||
let IP_ADDR_IPv4_INTERFACE_NAME = "ipv4"
|
||||
let IP_ADDR_IPv6_INTERFACE_NAME = "ipv6"
|
||||
|
||||
func ipAddress(fromSockAddrData data: Data) -> String? {
|
||||
let socketAddress = data.withUnsafeBytes() { (pointer: UnsafePointer<sockaddr_in>) in
|
||||
return pointer.pointee
|
||||
}
|
||||
guard let resultCString = inet_ntoa(socketAddress.sin_addr) else {
|
||||
return nil
|
||||
}
|
||||
return String(cString: resultCString)
|
||||
}
|
||||
|
||||
func port(fromSockAddrData data: Data) -> UInt16 {
|
||||
let socketAddress = data.withUnsafeBytes() { (pointer: UnsafePointer<sockaddr_in>) in
|
||||
return pointer.pointee
|
||||
}
|
||||
return socketAddress.sin_port
|
||||
}
|
||||
|
||||
// + (NSString*)ipAddressFromSockAddrData:(NSData*)data {
|
||||
// struct sockaddr_in * socketAddress = (struct sockaddr_in *)data.bytes;
|
||||
// struct in_addr ipAsStruct = ((struct sockaddr_in)*(socketAddress)).sin_addr;
|
||||
// char *buff;
|
||||
// buff = inet_ntoa(ipAsStruct);
|
||||
// NSString *ip = [NSString stringWithUTF8String:buff];
|
||||
// return ip;
|
||||
// }
|
||||
//
|
||||
// + (uint16_t)portFromSockAddrData:(NSData*)data {
|
||||
// struct sockaddr_in * socketAddressPtr = (struct sockaddr_in *)data.bytes;
|
||||
// struct sockaddr_in socketAddress = ((struct sockaddr_in)*(socketAddressPtr));
|
||||
// return socketAddress.sin_port;
|
||||
//}
|
||||
|
||||
func getIPAddress(of hostname: String) -> String? {
|
||||
struct InternetProtocol {
|
||||
|
||||
guard let hostnameCString = hostname.cString(using: .ascii),
|
||||
let hostEntry = gethostbyname(hostnameCString)?.pointee,
|
||||
let hostAddressList = hostEntry.h_addr_list?.pointee else {
|
||||
return nil
|
||||
}
|
||||
static let IOS_CELLULAR_INTERFACE_NAME = "pdp_ip0"
|
||||
static let IOS_WIFI_INTERFACE_NAME = "en0"
|
||||
static let IP_ADDR_IPv4_INTERFACE_NAME = "ipv4"
|
||||
static let IP_ADDR_IPv6_INTERFACE_NAME = "ipv6"
|
||||
|
||||
let firstHostAddress = hostAddressList.withMemoryRebound(to: in_addr.self, capacity: 1) { $0.pointee }
|
||||
let firstHostAddressCString = inet_ntoa(firstHostAddress)!
|
||||
return String(cString: firstHostAddressCString)
|
||||
}
|
||||
|
||||
func getLocalIPAddress(preferIPv4: Bool = true) -> String? {
|
||||
|
||||
// Prefer wifi over cellular
|
||||
let searchArray = preferIPv4 ?
|
||||
[
|
||||
IOS_WIFI_INTERFACE_NAME + "/" + IP_ADDR_IPv4_INTERFACE_NAME,
|
||||
IOS_WIFI_INTERFACE_NAME + "/" + IP_ADDR_IPv6_INTERFACE_NAME,
|
||||
IOS_CELLULAR_INTERFACE_NAME + "/" + IP_ADDR_IPv4_INTERFACE_NAME,
|
||||
IOS_CELLULAR_INTERFACE_NAME + "/" + IP_ADDR_IPv6_INTERFACE_NAME,
|
||||
] :
|
||||
[
|
||||
IOS_WIFI_INTERFACE_NAME + "/" + IP_ADDR_IPv6_INTERFACE_NAME,
|
||||
IOS_WIFI_INTERFACE_NAME + "/" + IP_ADDR_IPv4_INTERFACE_NAME,
|
||||
IOS_CELLULAR_INTERFACE_NAME + "/" + IP_ADDR_IPv6_INTERFACE_NAME,
|
||||
IOS_CELLULAR_INTERFACE_NAME + "/" + IP_ADDR_IPv4_INTERFACE_NAME,
|
||||
]
|
||||
|
||||
let addresses = getLocalIPAddresses()
|
||||
|
||||
for searchItem in searchArray {
|
||||
if let result = addresses[searchItem] {
|
||||
return result
|
||||
static func ipAddress(fromSockAddrData data: Data) -> String? {
|
||||
let socketAddress = data.withUnsafeBytes() { (pointer: UnsafePointer<sockaddr_in>) in
|
||||
return pointer.pointee
|
||||
}
|
||||
guard let resultCString = inet_ntoa(socketAddress.sin_addr) else {
|
||||
return nil
|
||||
}
|
||||
return String(cString: resultCString)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalIPAddresses() -> [String: String] {
|
||||
|
||||
var addresses: [String: String] = [:]
|
||||
|
||||
// Get list of all network interfaces on the local machine:
|
||||
var ifaddrsPointer : UnsafeMutablePointer<ifaddrs>?
|
||||
guard getifaddrs(&ifaddrsPointer) == 0, let ifaddrsList = ifaddrsPointer?.pointee else {
|
||||
return [:]
|
||||
static func port(fromSockAddrData data: Data) -> UInt16 {
|
||||
let socketAddress = data.withUnsafeBytes() { (pointer: UnsafePointer<sockaddr_in>) in
|
||||
return pointer.pointee
|
||||
}
|
||||
return socketAddress.sin_port
|
||||
}
|
||||
|
||||
// For each network interface ...
|
||||
for ifaddrs in ifaddrsList {
|
||||
if !ifaddrs.isUpAndRunning || ifaddrs.isLoopbackNet {
|
||||
continue
|
||||
static func getIPAddress(of hostname: String) -> String? {
|
||||
|
||||
guard let hostnameCString = hostname.cString(using: .ascii),
|
||||
let hostEntry = gethostbyname(hostnameCString)?.pointee,
|
||||
let hostAddressList = hostEntry.h_addr_list?.pointee else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ifaddrs.isIpv4 || ifaddrs.isIpv6 {
|
||||
if let addressString = ifaddrs.convertToIPString(), let name = ifaddrs.nameAndTypeString() {
|
||||
addresses[name] = addressString
|
||||
}
|
||||
}
|
||||
let firstHostAddress = hostAddressList.withMemoryRebound(to: in_addr.self, capacity: 1) { $0.pointee }
|
||||
let firstHostAddressCString = inet_ntoa(firstHostAddress)!
|
||||
return String(cString: firstHostAddressCString)
|
||||
}
|
||||
|
||||
freeifaddrs(ifaddrsPointer)
|
||||
static func getLocalIPAddress(preferIPv4: Bool = true) -> String? {
|
||||
|
||||
// Prefer wifi over cellular
|
||||
let searchArray = [
|
||||
ifaddrs.nameAndTypeString(from: IOS_WIFI_INTERFACE_NAME, isIpv4: preferIPv4),
|
||||
ifaddrs.nameAndTypeString(from: IOS_WIFI_INTERFACE_NAME, isIpv4: !preferIPv4),
|
||||
ifaddrs.nameAndTypeString(from: IOS_CELLULAR_INTERFACE_NAME, isIpv4: preferIPv4),
|
||||
ifaddrs.nameAndTypeString(from: IOS_CELLULAR_INTERFACE_NAME, isIpv4: !preferIPv4),
|
||||
]
|
||||
|
||||
let addresses = getLocalIPAddresses()
|
||||
|
||||
for searchItem in searchArray {
|
||||
if let result = addresses[searchItem] {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return addresses
|
||||
static func getLocalIPAddresses() -> [String: String] {
|
||||
|
||||
var addresses: [String: String] = [:]
|
||||
|
||||
// Get list of all network interfaces on the local machine:
|
||||
var ifaddrsPointer : UnsafeMutablePointer<ifaddrs>?
|
||||
guard getifaddrs(&ifaddrsPointer) == 0, let ifaddrsList = ifaddrsPointer?.pointee else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
// For each network interface ...
|
||||
for ifaddrs in ifaddrsList {
|
||||
if !ifaddrs.isUpAndRunning || ifaddrs.isLoopbackNet {
|
||||
continue
|
||||
}
|
||||
|
||||
if ifaddrs.isIpv4 || ifaddrs.isIpv6 {
|
||||
if let addressString = ifaddrs.convertToIPString(), let name = ifaddrs.nameAndTypeString() {
|
||||
addresses[name] = addressString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
freeifaddrs(ifaddrsPointer)
|
||||
|
||||
return addresses
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class ifaddrsIterator: IteratorProtocol {
|
||||
public typealias Element = ifaddrs
|
||||
|
||||
@@ -173,7 +155,13 @@ extension ifaddrs {
|
||||
return nil
|
||||
}
|
||||
|
||||
return "\(name) - " + (isIpv4 ? IP_ADDR_IPv4_INTERFACE_NAME : IP_ADDR_IPv6_INTERFACE_NAME)
|
||||
return ifaddrs.nameAndTypeString(from: name, isIpv4: isIpv4)
|
||||
}
|
||||
|
||||
static func nameAndTypeString(from name: String, isIpv4: Bool) -> String {
|
||||
return name + "/" + (isIpv4 ?
|
||||
InternetProtocol.IP_ADDR_IPv4_INTERFACE_NAME :
|
||||
InternetProtocol.IP_ADDR_IPv6_INTERFACE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,36 +12,36 @@ import XCTest
|
||||
class InternetProtocolTests: XCTestCase {
|
||||
|
||||
func test_canDecodeLocalhost() {
|
||||
let result = getIPAddress(of: "localhost")
|
||||
let result = InternetProtocol.getIPAddress(of: "localhost")
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result!, "127.0.0.1")
|
||||
}
|
||||
|
||||
func test_invalidHostnameReturnsNil() {
|
||||
let result = getIPAddress(of: "asldfjhablskhdbj")
|
||||
let result = InternetProtocol.getIPAddress(of: "asldfjhablskhdbj")
|
||||
XCTAssertNil(result)
|
||||
}
|
||||
|
||||
func test_nonAsciiHostnameReturnsNil() {
|
||||
let result = getIPAddress(of: "🙁")
|
||||
let result = InternetProtocol.getIPAddress(of: "🙁")
|
||||
XCTAssertNil(result)
|
||||
}
|
||||
|
||||
func test_canDecodeGoogle() {
|
||||
let result = getIPAddress(of: "google.com")
|
||||
let result = InternetProtocol.getIPAddress(of: "google.com")
|
||||
XCTAssertNotNil(result)
|
||||
}
|
||||
|
||||
func test_canDecodeIPv4AddressFromData() {
|
||||
let data = Data(bytes: [16,2,122,105,127,0,0,1,0,0,0,0,0,0,0,0])
|
||||
let result = ipAddress(fromSockAddrData: data)
|
||||
let result = InternetProtocol.ipAddress(fromSockAddrData: data)
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result, "127.0.0.1")
|
||||
}
|
||||
|
||||
func test_canDecodeSocketPortFromData() {
|
||||
let data = Data(bytes: [16,2,122,105,127,0,0,1,0,0,0,0,0,0,0,0])
|
||||
let result = port(fromSockAddrData: data)
|
||||
let result = InternetProtocol.port(fromSockAddrData: data)
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result, 27002)
|
||||
}
|
||||
|
||||
@@ -9,32 +9,32 @@
|
||||
import Foundation
|
||||
import CocoaAsyncSocket
|
||||
|
||||
protocol UDPConnectionProtocol: class {
|
||||
weak var delegate: UDPConnectionDelegate? { set get }
|
||||
func startListening(on port: UInt16)
|
||||
func send(_ data: Data, toHost host: String, port: UInt16, timeout: TimeInterval)
|
||||
}
|
||||
|
||||
protocol UDPConnectionDelegate: class {
|
||||
func udpConnection(_ sender: UDPConnection, receivedData data: Data, fromHost host: String)
|
||||
func udpConnection(_ sender: UDPConnectionProtocol, receivedData data: Data, fromHost host: String)
|
||||
}
|
||||
|
||||
/// This class is a thin wrapper around the socket library to protect against changes
|
||||
/// in its interface, and to allow me to replace CocoaAsyncSocket with a swift framework
|
||||
/// one day.
|
||||
class UDPConnection: NSObject {
|
||||
class UDPConnection: NSObject, UDPConnectionProtocol {
|
||||
|
||||
weak var delegate: UDPConnectionDelegate?
|
||||
|
||||
private let socket: GCDAsyncUdpSocket
|
||||
|
||||
// Designated init for testing
|
||||
init(socket: GCDAsyncUdpSocket) {
|
||||
init(socket: GCDAsyncUdpSocket = GCDAsyncUdpSocket()) {
|
||||
self.socket = socket
|
||||
super.init()
|
||||
socket.setDelegate(self)
|
||||
socket.synchronouslySetDelegateQueue(.main)
|
||||
}
|
||||
|
||||
// Useful init which should be used
|
||||
override convenience init() {
|
||||
self.init(socket: GCDAsyncUdpSocket())
|
||||
}
|
||||
|
||||
deinit {
|
||||
socket.close()
|
||||
}
|
||||
@@ -56,7 +56,7 @@ extension UDPConnection: GCDAsyncUdpSocketDelegate {
|
||||
fromAddress address: Data,
|
||||
withFilterContext filterContext: Any?) {
|
||||
|
||||
let hostString = ipAddress(fromSockAddrData: address)!
|
||||
let hostString = InternetProtocol.ipAddress(fromSockAddrData: address)!
|
||||
delegate?.udpConnection(self, receivedData: data, fromHost: hostString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import CocoaAsyncSocket
|
||||
class UDPConnectionDelegateTestingStub: UDPConnectionDelegate {
|
||||
|
||||
var receivedDataCalled = false
|
||||
var receivedDataParameters: (sender: UDPConnection, data: Data, host: String)?
|
||||
func udpConnection(_ sender: UDPConnection, receivedData data: Data, fromHost host: String) {
|
||||
var receivedDataParameters: (sender: UDPConnectionProtocol, data: Data, host: String)?
|
||||
func udpConnection(_ sender: UDPConnectionProtocol, receivedData data: Data, fromHost host: String) {
|
||||
receivedDataCalled = true
|
||||
receivedDataParameters = (sender, data, host)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class UDPConnectionTests: XCTestCase {
|
||||
sut.udpSocket(socket, didReceive: packetData, fromAddress: addressData, withFilterContext: nil)
|
||||
|
||||
XCTAssert(delegate.receivedDataCalled)
|
||||
XCTAssertEqual(delegate.receivedDataParameters?.sender, sut)
|
||||
XCTAssert(delegate.receivedDataParameters?.sender as AnyObject === sut)
|
||||
XCTAssertEqual(delegate.receivedDataParameters?.data, packetData)
|
||||
XCTAssertEqual(delegate.receivedDataParameters?.host, "127.0.0.1")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// BEncodeTestMacros.swift
|
||||
// BEncode
|
||||
//
|
||||
// Created by Ben Davis on 22/08/2016.
|
||||
// Copyright © 2016 bendavisapps. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
enum BEncoderTestError: Error {
|
||||
case InvalidType
|
||||
}
|
||||
|
||||
public func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> [[T]],
|
||||
_ expression2: @autoclosure () throws -> [[T]],
|
||||
_ message: @autoclosure () -> String = "",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
let array1: [[T]] = try! expression1()
|
||||
let array2: [[T]] = try! expression2()
|
||||
XCTAssertEqual(array1.count, array2.count)
|
||||
for i in 0..<array1.count {
|
||||
let element1 = array1[i]
|
||||
let element2 = array2[i]
|
||||
|
||||
XCTAssertEqual(element1, element2)
|
||||
}
|
||||
}
|
||||
|
||||
public func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> [T],
|
||||
_ expression2: @autoclosure () throws -> [T],
|
||||
_ message: @autoclosure () -> String = "",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
let array1: [T] = try! expression1()
|
||||
let array2: [T] = try! expression2()
|
||||
XCTAssertEqual(array1.count, array2.count)
|
||||
for i in 0..<array1.count {
|
||||
let element1 = array1[i]
|
||||
let element2 = array2[i]
|
||||
|
||||
if let element1 = element1 as? Int, let element2 = element2 as? Int {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? String, let element2 = element2 as? String {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? Data, let element2 = element2 as? Data {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [T], let element2 = element2 as? [T] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [String:T], let element2 = element2 as? [String:T] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [Data:T], let element2 = element2 as? [Data:T] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func XCTAssertEqual<T, E>(_ expression1: @autoclosure () throws -> [E: T],
|
||||
_ expression2: @autoclosure () throws -> [E: T],
|
||||
_ message: @autoclosure () -> String = "",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
let dictionary1: [E: T] = try! expression1()
|
||||
let dictionary2: [E: T] = try! expression2()
|
||||
XCTAssertEqual(dictionary1.count, dictionary2.count)
|
||||
|
||||
let keys1 = [E](dictionary1.keys)
|
||||
let keys2 = [E](dictionary2.keys)
|
||||
XCTAssertEqual(keys1, keys2)
|
||||
|
||||
for key in keys1 {
|
||||
let element1 = dictionary1[key]
|
||||
let element2 = dictionary2[key]
|
||||
|
||||
if let element1 = element1 as? Int, let element2 = element2 as? Int {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? String, let element2 = element2 as? String {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? Data, let element2 = element2 as? Data {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [T], let element2 = element2 as? [T] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [String:T], let element2 = element2 as? [String:T] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [Data:T], let element2 = element2 as? [Data:T] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func XCTAssertEqual(_ element1: Any, element2: Any) throws {
|
||||
if let element1 = element1 as? Int, let element2 = element2 as? Int {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? String, let element2 = element2 as? String {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? Data, let element2 = element2 as? Data {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [Any], let element2 = element2 as? [Any] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [String:Any], let element2 = element2 as? [String:Any] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else if let element1 = element1 as? [Data:Any], let element2 = element2 as? [Data:Any] {
|
||||
XCTAssertEqual(element1, element2)
|
||||
} else {
|
||||
throw BEncoderTestError.InvalidType
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule Submodules/BEncodeSwift updated: b4036e2e33...7e104ab912
Reference in New Issue
Block a user