diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-12-04 09:12:58 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-12-04 09:12:58 -0800 |
| commit | 471546236b8991f61d26c0e3aa8ee48b83b983af (patch) | |
| tree | b74331a8c8ea61ee7e1eaedfc7bbd1686bbc4702 /Hotline/Network/HotlineClient.swift | |
| parent | 885119acdd27bd53ed3096285c3eca3e2d99ad70 (diff) | |
Starting to hash out a server view and tabs. Added research pdfs. etc.
Diffstat (limited to 'Hotline/Network/HotlineClient.swift')
| -rw-r--r-- | Hotline/Network/HotlineClient.swift | 432 |
1 files changed, 0 insertions, 432 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) - } - } -} |