diff options
Diffstat (limited to 'Hotline/Network')
| -rw-r--r-- | Hotline/Network/HotlineClient.swift | 432 | ||||
| -rw-r--r-- | Hotline/Network/HotlineProtocol.swift | 388 | ||||
| -rw-r--r-- | Hotline/Network/HotlineTracker.swift | 260 |
3 files changed, 0 insertions, 1080 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift deleted file mode 100644 index b5092a0..0000000 --- a/Hotline/Network/HotlineClient.swift +++ /dev/null @@ -1,432 +0,0 @@ -import Foundation -import Network - -enum HotlineClientStatus: Int { - case disconnected - case connecting - case connected - case loggingIn - case loggedIn -} - -class HotlineClient : ObservableObject { - static let shared = HotlineClient() - - static let handshakePacket = Data([ - 0x54, 0x52, 0x54, 0x50, // 'TRTP' protocol ID - 0x48, 0x4F, 0x54, 0x4C, // Sub-protocol ID - 0x00, 0x01, // Version - 0x00, 0x02, // Sub-version - ]) - - @Published var connectionStatus: HotlineClientStatus = .disconnected - @Published var agreement: String? - @Published var userList: [HotlineUser] = [] - @Published var chatMessages: [String] = [] - - var userName: String = "bolt" - var userIconID: UInt16 = 128 - var serverVersion: UInt16 = 151 - var server: HotlineServer? - var connection: NWConnection? - - private var transactionLog: [UInt32:HotlineTransactionType] = [:] - - init() { - - } - - // MARK: - - - func connect(to server: HotlineServer) { - self.server = server - - let serverAddress = NWEndpoint.Host(server.address) - let serverPort = NWEndpoint.Port(rawValue: server.port)! - - let tcpOptions = NWProtocolTCP.Options() - tcpOptions.enableKeepalive = true - tcpOptions.keepaliveInterval = 30 - let connectionParameters: NWParameters - connectionParameters = NWParameters(tls: nil, tcp: tcpOptions) - - self.connection = NWConnection(host: serverAddress, port: serverPort, using: connectionParameters) - self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in - switch newState { - case .ready: - print("HotlineClient: connection ready!") - DispatchQueue.main.async { - self?.connectionStatus = .connected - } - self?.sendHandshake() - case .cancelled: - print("HotlineClient: connection cancelled") - DispatchQueue.main.async { - self?.connectionStatus = .disconnected - } - case .failed(let err): - print("HotlineClient: connection error \(err)") - DispatchQueue.main.async { - self?.connectionStatus = .disconnected - } - default: - print("HotlineClient: unhandled connection state \(newState)") - } - } - - DispatchQueue.main.async { - self.connectionStatus = .connecting - } - self.connection?.start(queue: .global()) - } - - private func disconnect() { - self.connection?.cancel() - self.connection = nil - - DispatchQueue.main.async { - self.connectionStatus = .disconnected - } - } - - // MARK: - - - private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil) { - guard let c = connection else { - return - } - - print("HotlineClient => \(t.id) \(t.type)") - - self.transactionLog[t.id] = t.type - - c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in - if disconnectOnError, error != nil { - self?.disconnect() - return - } - - callback?() - }) - } - - private func receiveTransaction() { - guard let c = connection else { - print("HotlineClient: no connection for transaction.") - return - } - - print("HotlineClient ⏳") - c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (headerData, context, isComplete, error) in - guard let self = self else { - return - } - - if let error = error { - print("HotlineClient: transaction error \(error)") - self.disconnect() - return - } - - guard let headerData = headerData, !headerData.isEmpty else { - self.receiveTransaction() - return - } - -// print("HotlineClient: received \(headerData.count) header bytes") - - if var transaction = self.parseTransaction(data: headerData) { - // Receive additional data if the transaction has data attached to it. - print("DATA SIZE: \(transaction.dataSize)") - if transaction.dataSize > 0 { - c.receive(minimumIncompleteLength: Int(transaction.dataSize), maximumLength: Int(transaction.dataSize)) { [weak self] (fieldData, context, isComplete, error) in - guard let self = self else { - return - } - - guard let fieldData = fieldData, !fieldData.isEmpty else { - print("HotlineClient: transaction field data is empty!") - self.disconnect() - return - } - - let fieldCount = fieldData.readUInt16(at: 0)! - - if fieldCount > 0 { - var dataCursor = 2 - for _ in 0..<fieldCount { - if - let fieldID = fieldData.readUInt16(at: dataCursor), - let fieldSize = fieldData.readUInt16(at: dataCursor + 2), - let fieldRemainingData = fieldData.readData(at: dataCursor + 4, length: Int(fieldSize)) { - - if let fieldType = HotlineTransactionFieldType(rawValue: fieldID) { - - transaction.fields.append(HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldRemainingData)) -// transaction.parameters[fieldType] = HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldData) - } - else { - print("HotlineClient: UNKNOWN FIELD TYPE!", fieldID, fieldSize) - } - - dataCursor += 4 + Int(fieldSize) - } - } - - // Process the transaction if we have processed more than zero parameters here - // as we expect parameters at this point. - self.processTransaction(transaction) - } - - // Continue receiving transactions. - self.receiveTransaction() - } - } - else { - // In this case we have no further data to receive so we simply - // process the transaction and then continue receiving. - self.processTransaction(transaction) - self.receiveTransaction() - } - } - else { - // Here we failed to parse the current transaction. - // We should consider disconnecting perhaps. - // But for now we'll continue receiving. - self.receiveTransaction() - } - } - } - - private func parseTransaction(data: Data) -> HotlineTransaction? { - if - let flags = data.readUInt8(at: 0), - let isReply = data.readUInt8(at: 1), - let type = data.readUInt16(at: 2), - let id = data.readUInt32(at: 4), - let errorCode = data.readUInt32(at: 8), - let transactionSize = data.readUInt32(at: 12), - let dataSize = data.readUInt32(at: 16) { - - if let transactionType = HotlineTransactionType(rawValue: type) { - return HotlineTransaction(type: transactionType, flags: flags, isReply: isReply, id: id, errorCode: errorCode, totalSize: transactionSize, dataSize: dataSize) - } - else { - print("HotlineClient: Unknown type \(type) parsing with \(dataSize) bytes") - } - } - - return nil - } - - // MARK: - Messages - - private func sendHandshake() { - guard let c = connection else { - print("HotlineClient: invalid connection to send handshake.") - return - } - - c.send(content: HotlineClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in - if let err = error { - print("HotlineClient: sending handshake failed \(err)") - return - } - print("HotlineClient: receiving handshake...") - c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in - guard let self = self, let data = data else { - return - } - - if data.isEmpty { - print("HotlineClient: empty handshake response") - self.disconnect() - return - } - - let protocolID = data.readUInt32(at: 0)! - if protocolID != 0x54525450 { // 'TRTP' - print("HotlineClient: invalid handshake protocol ID \(protocolID)") - self.disconnect() - return - } - - let errorCode = data.readUInt32(at: 4)! - if errorCode != 0 { // 0 == no error - print("HotlineClient: handshake error", errorCode) - self.disconnect() - return - } - - print("HotlineClient 🤝") - self.sendLogin() { [weak self] in - self?.sendSetClientUserInfo() - self?.sendGetUserList() - } - self.receiveTransaction() - } - }) - } - - func sendLogin(callback: (() -> Void)? = nil) { - DispatchQueue.main.async { - self.connectionStatus = .loggingIn - } - - var t = HotlineTransaction(type: .login) - t.setFieldEncodedString(type: .userLogin, val: "") - t.setFieldEncodedString(type: .userPassword, val: "") - t.setFieldUInt16(type: .userIconID, val: self.userIconID) - t.setFieldString(type: .userName, val: self.userName) - t.setFieldUInt32(type: .versionNumber, val: 123) - - self.sendTransaction(t) { [weak self] in - DispatchQueue.main.async { - self?.connectionStatus = .loggedIn - } - - callback?() - } - } - - func sendSetClientUserInfo(callback: (() -> Void)? = nil) { - var t = HotlineTransaction(type: .setClientUserInfo) - t.setFieldString(type: .userName, val: self.userName) - t.setFieldUInt16(type: .userIconID, val: self.userIconID) - - self.sendTransaction(t, callback: callback) - } - - func sendAgree(callback: (() -> Void)? = nil) { - var t = HotlineTransaction(type: .agreed) - t.setFieldString(type: .userName, val: self.userName) - t.setFieldUInt16(type: .userIconID, val: self.userIconID) - t.setFieldUInt32(type: .options, val: 0) - self.sendTransaction(t, callback: callback) - } - - func sendChat(message: String, callback: (() -> Void)? = nil) { - var t = HotlineTransaction(type: .sendChat) - t.setFieldString(type: .data, val: message) - self.sendTransaction(t, callback: callback) - } - - func sendGetUserList(callback: (() -> Void)? = nil) { - let t = HotlineTransaction(type: .getUserNameList) - self.sendTransaction(t, callback: callback) - } - - // MARK: - Incoming - - private func processReply(_ transaction: HotlineTransaction) { - guard transaction.errorCode == 0 else { - if let errorParam = transaction.getField(type: .errorText), let errorText = errorParam.getString() { - print("HotlineClient 😵 \(transaction.errorCode): \(errorText)") - } - else { - print("HotlineClient 😵 \(transaction.errorCode)") - } - return - } - - guard let repliedTransactionType = self.transactionLog[transaction.id] else { - return - } - - print("HotlineClient reply in response to \(repliedTransactionType)") - - switch(repliedTransactionType) { - case .login: - print("GOT REPLY TO LOGIN!") - - if - let serverVersionField = transaction.getField(type: .versionNumber), - let serverVersion = serverVersionField.getUInt16() { - self.serverVersion = serverVersion - print("SERVER VERSION: \(serverVersion)") - } - case .getUserNameList: - print("GOT USER LIST") - var newUserList: [HotlineUser] = [] - for u in transaction.getFieldList(type: .userNameWithInfo) { - let info = u.getUserInfo() - print("USER: \(info)") - let user = HotlineUser(id: info.id, iconID: info.iconID, status: info.flags, name: info.userName) - newUserList.append(user) - } - DispatchQueue.main.async { - self.userList = newUserList - } - default: - break - } - } - - private func processTransaction(_ transaction: HotlineTransaction) { - if transaction.type == .reply { - print("HotlineClient <= \(transaction.type) to \(transaction.id):") - print(transaction) - } - else { - print("HotlineClient <= \(transaction.type)") - } - - switch(transaction.type) { - case .reply: - self.processReply(transaction) -// print("HotlineClient: Received reply transaction: \(transaction)") - - case .chatMessage: - if - let chatTextParam = transaction.getField(type: .data), - let chatText = chatTextParam.getString(), - let userNameParam = transaction.getField(type: .userName), - let userName = userNameParam.getString(), - let userIDParam = transaction.getField(type: .userID), - let userID = userIDParam.getUInt16() { - print("HotlineClient: \(userName):\(userID): \(chatText)") - DispatchQueue.main.async { - self.chatMessages.append(chatText) - } - } - case .getUserNameList: - print("HotlineClient: GOT USER NAME LIST!") - let userList = transaction.getFieldList(type: .userNameWithInfo) - for u in userList { - let userInfo = u.getUserInfo() - print("HotlineClient: user \(userInfo.userName)") - } - case .notifyOfUserChange: -// print("HotlineClient: user changed") - if let p = transaction.getField(type: .userName), - let userName = p.getString() { - print("HotlineClient: User changed \(userName)") - } - case .disconnectMessage: - print("HotlineClient ❌") - self.disconnect() - case .showAgreement: - if let noAgreementField = transaction.getField(type: .noServerAgreement) { - print("NO AGREEMENT?") - } - if let agreementParam = transaction.getField(type: .data) { - if let agreementText = agreementParam.getString() { - print("\n\n--------------------------\n") - print(agreementText) - print("\n--------------------------\n\n") - DispatchQueue.main.async { - self.agreement = agreementText - } -// self.sendAgree() { -// self.sendGetUserList() -// } - } - } - case .userAccess: - print("") - default: - print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.fields.count) parameters") - print(transaction.fields) - } - } -} diff --git a/Hotline/Network/HotlineProtocol.swift b/Hotline/Network/HotlineProtocol.swift deleted file mode 100644 index 5372e78..0000000 --- a/Hotline/Network/HotlineProtocol.swift +++ /dev/null @@ -1,388 +0,0 @@ -import Foundation - -struct HotlineServer: Identifiable, Hashable { - let id = UUID() - let address: String - let port: UInt16 - let users: UInt16 - let name: String? - let description: String? - - static func == (lhs: HotlineServer, rhs: HotlineServer) -> Bool { - return lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } -} - -struct HotlineUser: Identifiable, Hashable { - let id: UInt16 - let iconID: UInt16 - let status: UInt16 - let name: String - - var isAdmin: Bool { - return ((self.status & 0x0002) != 0) - } - - var isIdle: Bool { - return ((self.status & 0x0001) != 0) - } - - static func == (lhs: HotlineUser, rhs: HotlineUser) -> Bool { - return lhs.id == rhs.id - } - - init(id: UInt16, iconID: UInt16, status: UInt16, name: String) { - self.id = id - self.iconID = iconID - self.status = status - self.name = name - } - - init(from data: Data) { - self.id = data.readUInt16(at: 0)! - self.iconID = data.readUInt16(at: 2)! - self.status = data.readUInt16(at: 4)! - - let userNameLength = Int(data.readUInt16(at: 6)!) - self.name = data.readString(at: 8, length: userNameLength, encoding: .ascii)! - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } - - func encoded() -> Data { - var data = Data() - self.encode(to: &data) - return data - } - - func encode(to data: inout Data) { - data.appendUInt16(self.id) - data.appendUInt16(self.iconID) - data.appendUInt16(self.status) - - let userNameData = name.data(using: .ascii, allowLossyConversion: true)! - - data.appendUInt16(UInt16(userNameData.count)) - data.append(userNameData) - } -} - -struct HotlineUserInfo { - let id: UInt16 - let iconID: UInt16 - let flags: UInt16 - let userName: String - - init(data: Data) { - self.id = data.readUInt16(at: 0)! - self.iconID = data.readUInt16(at: 2)! - self.flags = data.readUInt16(at: 4)! - - let userNameLength = data.readUInt16(at: 6)! - self.userName = data.readString(at: 8, length: Int(userNameLength), encoding: .ascii)! - } -} - -struct HotlineTransactionField { - let type: HotlineTransactionFieldType - let dataSize: UInt16 - let data: Data - - init(type: HotlineTransactionFieldType, dataSize: UInt16, data: Data) { - self.type = type - self.dataSize = dataSize - self.data = data - } - - init(type: HotlineTransactionFieldType, val: UInt8) { - self.init(type: type, dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val)) - } - - init(type: HotlineTransactionFieldType, val: UInt16) { - self.init(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val)) - } - - init(type: HotlineTransactionFieldType, val: UInt32) { - self.init(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val)) - } - - init(type: HotlineTransactionFieldType, string: String, encoding: String.Encoding = .ascii, encrypt: Bool = false) { - var stringInput = string - - if encrypt { - stringInput = String(string.utf8.map { char in - Character(UnicodeScalar(0xFF - char)) - }) - } - - var stringData: Data? - stringData = stringInput.data(using: encoding, allowLossyConversion: true) - if stringData == nil { - stringData = Data() - } - - self.init(type: type, dataSize: UInt16(stringData!.count), data: stringData!) - } - - init(type: HotlineTransactionFieldType, string: String, encrypt: Bool) { - self.init(type: type, string: string, encoding: .ascii, encrypt: encrypt) - } - - func getUInt8() -> UInt8? { - return self.data.readUInt8(at: 0) - } - - func getUInt16() -> UInt16? { - return self.data.readUInt16(at: 0) - } - - func getUInt32() -> UInt32? { - return self.data.readUInt32(at: 0) - } - - func getInteger() -> Int? { - switch(self.data.count) { - case 1: - if let val = self.getUInt8() { - return Int(val) - } - case 2: - if let val = self.getUInt16() { - return Int(val) - } - case 4: - if let val = self.getUInt32() { - return Int(val) - } - default: - break - } - - return nil - } - - func getString(encoding: String.Encoding = .ascii) -> String? { - return String(data: self.data, encoding: encoding) - } - - func getUserInfo() -> HotlineUserInfo { - return HotlineUserInfo(data: self.data) - } -} - -struct HotlineTransaction { - static let headerSize = 20 - static var sequenceID: UInt32 = 1 - - static func nextID() -> UInt32 { - HotlineTransaction.sequenceID += 1 - return HotlineTransaction.sequenceID - } - - var flags: UInt8 = 0 - var isReply: UInt8 = 0 - var type: HotlineTransactionType - var id: UInt32 = HotlineTransaction.nextID() - var errorCode: UInt32 = 0 - var totalSize: UInt32 = UInt32(HotlineTransaction.headerSize) - var dataSize: UInt32 = 0 - - var fields: [HotlineTransactionField] = [] - - init(type: HotlineTransactionType) { - self.type = type - } - - init(type: HotlineTransactionType, flags: UInt8, isReply: UInt8, id: UInt32, errorCode: UInt32, totalSize: UInt32, dataSize: UInt32) { - self.type = type - self.flags = flags - self.isReply = isReply - self.id = id - self.errorCode = errorCode - self.totalSize = totalSize - self.dataSize = dataSize - } - - mutating func setFieldUInt8(type: HotlineTransactionFieldType, val: UInt8) { - self.fields.append(HotlineTransactionField(type: type, val: val)) - } - - mutating func setFieldUInt16(type: HotlineTransactionFieldType, val: UInt16) { - self.fields.append(HotlineTransactionField(type: type, val: val)) - } - - mutating func setFieldUInt32(type: HotlineTransactionFieldType, val: UInt32) { - self.fields.append(HotlineTransactionField(type: type, val: val)) - } - - mutating func setFieldEncodedString(type: HotlineTransactionFieldType, val: String) { - self.fields.append(HotlineTransactionField(type: type, string: val, encrypt: true)) - } - - mutating func setFieldString(type: HotlineTransactionFieldType, val: String) { - self.fields.append(HotlineTransactionField(type: type, string: val)) - } - - func getField(type: HotlineTransactionFieldType) -> HotlineTransactionField? { - return self.fields.first { p in - p.type == type - } - } - - func getFieldList(type: HotlineTransactionFieldType) -> [HotlineTransactionField] { - return self.fields.filter { p in - p.type == type - } - } - - func encoded() -> Data { - var data = Data() - self.encode(to: &data) - return data - } - - func encode(to data: inout Data) { - data.appendUInt8(self.flags) - data.appendUInt8(self.isReply) - data.appendUInt16(self.type.rawValue) - data.appendUInt32(self.id) - data.appendUInt32(self.errorCode) - - if self.fields.count > 0 { - var fieldData = Data() - fieldData.appendUInt16(UInt16(self.fields.count)) - for f in self.fields { - fieldData.appendUInt16(f.type.rawValue) - fieldData.appendUInt16(f.dataSize) - fieldData.append(f.data) - } - - data.appendUInt32(UInt32(fieldData.count)) - data.appendUInt32(UInt32(fieldData.count)) - data.append(fieldData) - } - else { - data.appendUInt32(2) - data.appendUInt32(2) - data.appendUInt16(0) - } - } -} - -enum HotlineTransactionFieldType: UInt16 { - case errorText = 100 // String - case data = 101 // String - case userName = 102 // String - case userID = 103 // Integer - case userIconID = 104 // Integer - case userLogin = 105 // Encoded string - case userPassword = 106 // Encoded string - case referenceNumber = 107 // Integer - case transferSize = 108 // Integer - case chatOptions = 109 // Integer - case userAccess = 110 // 64-bit integer? - case userAlias = 111 // ??? - case userFlags = 112 // Integer - case options = 113 // 32-bit integer? - case chatID = 114 // Integer - case chatSubject = 115 // String - case waitingCount = 116 // Integer - case serverAgreement = 150 // ??? - case serverBanner = 151 // Data? - case serverBannerType = 152 // Integer - case serverBannerURL = 153 // String - case noServerAgreement = 154 // Integer - case versionNumber = 160 // Integer - case communityBannerID = 161 // Integer - case serverName = 162 // String - // TODO: Add file field types - case quotingMessage = 214 // String? - case automaticResponse = 215 // String - case folderItemCount = 220 // Integer - case userNameWithInfo = 300 // Data { user id: 2, icon id: 2, user flags: 2, user name size: 2, user name: size } - case newsCategoryGUID = 319 // Data? - case newsCategoryListData = 320 // Data { type: 1 (1 = folder, 10 = category, 255 = other), category name: rest } - case newsArticleListData = 321 // Data - case newsCategoryName = 322 // String - case newsCategoryListData15 = 323 // Data - case newsPath = 325 // Data - case newsArticleID = 326 // Integer - case newsArticleDataFlavor = 327 // String - case newsArticleTitle = 328 // String - case newsArticlePoster = 329 // String - case newsArticleDate = 330 // Data { year: 2, ms: 2, secs: 4 } - case newsArticlePrevious = 331 // Integer - case newsArticleNext = 332 // Integer - case newsArticleData = 333 // Data - case newsArticleFlags = 334 // Integer - case newsArticleParentArticle = 335 // Integer - case newsArticleFirstChildArticle = 336 // Integer - case newsArticleRecursiveDelete = 337 // Integer -} - -enum HotlineTransactionType: UInt16 { - case reply = 0 - case error = 100 - case getMessages = 101 - case newMessage = 102 // Server - case oldPostNews = 103 - case serverMessage = 104 // Server - case sendChat = 105 - case chatMessage = 106 // Server - case login = 107 - case sendInstantMessage = 108 - case showAgreement = 109 // Server - case disconnectUser = 110 - case disconnectMessage = 111 // Server - case inviteToNewChat = 112 - case inviteToChat = 113 // Server - case rejectChatInvite = 114 - case joinChat = 115 - case leaveChat = 116 - case notifyChatOfUserChange = 117 // Server - case notifyChatOfUserDelete = 118 // Server - case notifyChatSubject = 119 // Server - case setChatSubject = 120 - case agreed = 121 - case serverBanner = 122 // Server - case getFileNameList = 200 - case downloadFile = 202 - case uploadFile = 203 - case deleteFile = 204 - case newFolder = 205 - case getFileInfo = 206 - case setFileInfo = 207 - case moveFile = 208 - case makeFileAlias = 209 - case downloadFolder = 210 - case downloadInfo = 211 // Server - case downloadBanner = 212 - case uploadFolder = 213 - case getUserNameList = 300 - case notifyOfUserChange = 301 // Server - case notifyOfUserDelete = 302 // Server - case getClientInfoText = 303 - case setClientUserInfo = 304 - case newUser = 350 - case deleteUser = 351 - case getUser = 352 - case setUser = 353 - case userAccess = 354 // Server - case userBroadcast = 355 // Client & Server - case getNewsCategoryNameList = 370 - case getNewsArticleNameList = 371 - case deleteNewsItem = 380 - case newNewsFolder = 381 - case newNewsCategory = 382 - case getNewsArticleData = 400 - case postNewsArticle = 410 - case deleteNewsArticle = 411 - case connectionKeepAlive = 500 -} - diff --git a/Hotline/Network/HotlineTracker.swift b/Hotline/Network/HotlineTracker.swift deleted file mode 100644 index 2c44bc8..0000000 --- a/Hotline/Network/HotlineTracker.swift +++ /dev/null @@ -1,260 +0,0 @@ -import Foundation -import Network - -enum HotlineTrackerStatus: Int { - case disconnected - case connecting - case connected -} - -class HotlineTracker : ObservableObject { - let serverAddress: NWEndpoint.Host - let serverPort: NWEndpoint.Port = NWEndpoint.Port(rawValue: 5498)! - - static let magicPacket = Data([ - 0x48, 0x54, 0x52, 0x4B, // 'HTRK' - 0x00, 0x01 // Version - ]) - - var connection: NWConnection? - - var bytes = Data() - var maxDataLength: Int = 0 - var serverCount: Int = 0 - - @Published var connectionStatus: HotlineTrackerStatus = .disconnected - @Published var servers: [HotlineServer] = [] - - init(address: String) { - self.serverAddress = NWEndpoint.Host(address) - } - - func fetch() { - self.reset() - self.connect() - } - - private func reset() { - self.maxDataLength = 0 - self.serverCount = 0 - } - - private func connect() { - self.connection = NWConnection(host: self.serverAddress, port: self.serverPort, using: .tcp) - self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in - switch newState { - case .ready: - print("READY TO SEND AND RECEIVE DATA") - DispatchQueue.main.async { - self?.connectionStatus = .connected - } - self?.sendMagic() - case .cancelled: - print("CONNECTION CANCELLED") - DispatchQueue.main.async { - self?.connectionStatus = .disconnected - } - case .failed(let err): - print("CONNECTION ERROR \(err)") - DispatchQueue.main.async { - self?.connectionStatus = .disconnected - } - default: - print("CONNECTION OTHER THING") - } - } - - DispatchQueue.main.async { - self.connectionStatus = .connecting - } - self.connection?.start(queue: .global()) - } - - private func disconnect() { - guard let c = connection else { - print("HotlineTracker: already disconnected") - return - } - - c.cancel() - self.connection = nil - } - - private func sendMagic() { - guard let c = connection else { - print("HotlineTracker: invalid connection to send magic.") - return - } - - // let packet: [UInt8] = [0x48, 0x54, 0x52, 0x4B, 0x00, self.serverVersion] - - c.send(content: HotlineTracker.magicPacket, completion: .contentProcessed { [weak self] (error) in - if let err = error { - print("HotlineTracker: sending magic failed \(err)") - return - } - - print("HotlineTracker: sent magic!") - - self?.receiveMagic() - }) - } - - private func receiveMagic() { - guard let c = connection else { - print("HotlineTracker: invalid connection to receive magic.") - return - } - - print("HotlineTracker: receiving...") - c.receive(minimumIncompleteLength: 6, maximumLength: 6) { [weak self] (data, context, isComplete, error) in - guard let self = self, let data = data else { - return - } - - if data.isEmpty || !data.elementsEqual(HotlineTracker.magicPacket) { - print("HotlineTracker: invalid magic response") - self.disconnect() - return - } - // if let data = data, !data.isEmpty { - print("HotlineTracker: received magic response!") - // } - - if let error = error { - print("HotlineTracker: receive error \(error)") - } - else { - self.receiveHeader() - } - } - } - - private func receiveHeader() { - guard let c = connection else { - print("HotlineTracker: invalid connection to receive header.") - return - } - - print("HotlineTracker: receiving...") - c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in - guard let self = self else { - return - } - - if let error = error { - print("HotlineTracker: receive error \(error)") - self.disconnect() - return - } - - if let data = data, !data.isEmpty { - print("HotlineTracker: received \(data.count) header bytes") - - self.maxDataLength = Int(data[2]) * 0xFF + Int(data[3]) - self.maxDataLength -= 4 - print("HotlineTracker: message size = \(self.maxDataLength)") - - self.serverCount = Int(data[4]) * 256 + Int(data[5]) - print("HotlineTracker: server count = \(self.serverCount)") - } - - if let error = error { - print("HotlineTracker: receive error \(error)") - } - else { - self.receiveListing() - } - } - } - - private func receiveListing() { - guard let c = connection else { - print("HotlineTracker: invalid connection to receive data.") - return - } - - print("HotlineTracker: receiving...") - c.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { [weak self] (data, context, isComplete, error) in - guard let self = self else { - return - } - - if let data = data, !data.isEmpty { - print("HotlineTracker: received \(data.count) bytes") - self.bytes.append(contentsOf: data) - - if bytes.count >= maxDataLength { - print("HotlineTracker: done with data, should close. \(self.bytes.count) \(self.maxDataLength)") - self.disconnect() - self.parseListing() - return - } - } - - if let error = error { - print("HotlineTracker: receive error \(error)") - self.disconnect() - } - else { - print("HotlineTracker: not complete") - self.receiveListing() - } - } - } - - private func parseListing() { - // IP address (4 bytes) - // Port number (2 bytes) - // Number of users (2 bytes) - // Unused (2 bytes) - // Name size (1 byte) - // Name (name size) - // Description size (1 byte) - // Description (description size) - - var foundServers: [HotlineServer] = [] - - var cursor = 0 - for _ in 1...self.serverCount { - if self.bytes.count < cursor + 12 { - print("HotlineTracker: Data isn't long enough for next server") - break - } - - if - let ip_1 = self.bytes.readUInt8(at: cursor), - let ip_2 = self.bytes.readUInt8(at: cursor + 1), - let ip_3 = self.bytes.readUInt8(at: cursor + 2), - let ip_4 = self.bytes.readUInt8(at: cursor + 3), - let port = self.bytes.readUInt16(at: cursor + 4), - let userCount = self.bytes.readUInt16(at: cursor + 6), - let nameLengthByte = self.bytes.readUInt8(at: cursor + 10) { - - let nameLength = Int(nameLengthByte) - if let name = self.bytes.readString(at: cursor + 11, length: nameLength, encoding: .utf8) ?? self.bytes.readString(at: cursor + 11, length: nameLength, encoding: .ascii) { - if let descLengthByte = self.bytes.readUInt8(at: cursor + 11 + nameLength) { - let descLength = Int(descLengthByte) - if let desc = self.bytes.readString(at: cursor + 11 + nameLength + 1, length: descLength, encoding: .utf8) ?? self.bytes.readString(at: cursor + 11 + nameLength + 1, length: descLength, encoding: .ascii) { - let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc) - - print("SERVER: \(server)") - - foundServers.append(server) - - cursor += 11 + nameLength + 1 + descLength - } - } - } - } - - print(cursor) - } - - DispatchQueue.main.async { -// print("CALLING CALLBACK") - self.servers = foundServers - } - - } -} |