diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-11-29 20:13:23 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-11-29 20:13:23 -0800 |
| commit | 30255476781789df318a571622ab732681696139 (patch) | |
| tree | 7e05705c09c1a455ca6573af686f9eb0629df894 /Hotline/Network/HotlineClient.swift | |
| parent | 2bdcc0c3d751d7c7626d8b0e9f79331b803c668b (diff) | |
Further work on protocol.
Diffstat (limited to 'Hotline/Network/HotlineClient.swift')
| -rw-r--r-- | Hotline/Network/HotlineClient.swift | 364 |
1 files changed, 242 insertions, 122 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift index d3b6c98..5193fe5 100644 --- a/Hotline/Network/HotlineClient.swift +++ b/Hotline/Network/HotlineClient.swift @@ -5,6 +5,8 @@ enum HotlineClientStatus: Int { case disconnected case connecting case connected + case loggingIn + case loggedIn } class HotlineClient : ObservableObject { @@ -16,25 +18,37 @@ class HotlineClient : ObservableObject { 0x00, 0x01, // Version 0x00, 0x02, // Sub-version ]) - + @Published var connectionStatus: HotlineClientStatus = .disconnected + @Published var agreement: String? + @Published var userList: [HotlineUser] = [] + @Published var chatMessages: [String] = [] + + let userName: String = "bolt" + let userIconID: UInt32 = 128 var server: HotlineServer? var connection: NWConnection? - var bytes = Data() - var handshakeComplete = false init() { } + // MARK: - + func connect(to server: HotlineServer) { self.server = server let serverAddress = NWEndpoint.Host(server.address) let serverPort = NWEndpoint.Port(rawValue: server.port)! - self.connection = NWConnection(host: serverAddress, port: serverPort, using: .tcp) + 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: @@ -65,85 +79,28 @@ class HotlineClient : ObservableObject { } private func disconnect() { - guard let c = connection else { - print("HotlineClient: already disconnected") - return - } - - c.cancel() + self.connection?.cancel() self.connection = nil - } - - 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 magic failed \(err)") - return - } - - print("HotlineClient: sent handshake packet!") - - self?.receiveHandshake() - }) - } - - private func receiveHandshake() { - guard let c = connection else { - print("HotlineTracker: invalid connection to receive magic.") - 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: completed handshake") - self.sendLogin() + DispatchQueue.main.async { + self.connectionStatus = .disconnected } } - private func sendLogin() { + // MARK: - + + private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil) { guard let c = connection else { - print("HotlineClient: no connection for transaction.") return } - c.send(content: HotlineClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in - if let err = error { - print("HotlineClient: sending login failed \(err)") + c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in + if disconnectOnError, error != nil { + self?.disconnect() return } - print("HotlineClient: sent handshake packet!") - - + callback?() }) } @@ -153,67 +110,86 @@ class HotlineClient : ObservableObject { return } - print("HotlineClient: waiting on transaction header...") - c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (data, context, isComplete, error) in + print("HotlineClient: waiting for transaction...") + 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: receive error \(error)") + print("HotlineClient: transaction error \(error)") self.disconnect() return } - if let data = data, !data.isEmpty { - print("HotlineClient: received \(data.count) header bytes") - - let transaction = self.parseTransaction(data: data) - if var t = transaction { - if t.dataSize > 0 { - c.receive(minimumIncompleteLength: Int(t.dataSize), maximumLength: Int(t.dataSize)) { [weak self] (data, context, isComplete, error) in - guard let self = self else { - return - } - - if let data = data, !data.isEmpty { - t.parameterCount = data.readUInt16(at: 0)! - - if t.parameterCount > 0 { - t.parameters = [] - var dataCursor = 2 - for _ in 0..<t.parameterCount { - if - let fieldID = data.readUInt16(at: dataCursor), - let fieldSize = data.readUInt16(at: dataCursor + 2), - let fieldData = data.readData(at: dataCursor + 4, length: Int(fieldSize)) { - t.parameters?.append(HotlineTransactionParameter(id: fieldID, dataSize: fieldSize, data: fieldData)) - - dataCursor += 4 + Int(fieldSize) - } + 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] (parameterData, context, isComplete, error) in + guard let self = self else { + return + } + + guard let parameterData = parameterData, !parameterData.isEmpty else { + print("HotlineClient: transaction parameter data is empty!") + self.disconnect() + return + } + + let parameterCount = parameterData.readUInt16(at: 0)! + + if parameterCount > 0 { + var dataCursor = 2 + for _ in 0..<parameterCount { + if + let fieldID = parameterData.readUInt16(at: dataCursor), + let fieldSize = parameterData.readUInt16(at: dataCursor + 2), + let fieldData = parameterData.readData(at: dataCursor + 4, length: Int(fieldSize)) { + + if let fieldType = HotlineTransactionFieldType(rawValue: fieldID) { + transaction.parameters.append(HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData)) +// transaction.parameters[fieldType] = HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData) + } + else { + print("HotlineClient: UNKNOWN PARAM TYPE!", fieldID, fieldSize) } + + dataCursor += 4 + Int(fieldSize) } } - self.processTransaction(t) + // Process the transaction if we have processed more than zero parameters here + // as we expect parameters at this point. + self.processTransaction(transaction) } - } - else { - self.processTransaction(t) + + // Continue receiving transactions. + self.receiveTransaction() } } - -// print("HotlineTracker: server count = \(self.serverCount)") + 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() } - -// self.receiveListing() -// } } } - - private func processTransaction(_ transaction: HotlineTransaction) { - print("HotlineClient processing transaction \(transaction.type) with \(transaction.parameterCount) parameters") - } private func parseTransaction(data: Data) -> HotlineTransaction? { if @@ -225,17 +201,161 @@ class HotlineClient : ObservableObject { let transactionSize = data.readUInt32(at: 12), let dataSize = data.readUInt32(at: 16) { - return HotlineTransaction( - flags: flags, - isReply: isReply, - type: HotlineTransactionType(rawValue: type) ?? HotlineTransactionType.unknown, - id: id, - errorCode: errorCode, - totalSize: transactionSize, - dataSize: dataSize - ) + print("HotlineClient: Parsing transaction type \(type) with data \(dataSize)") + if let transactionType = HotlineTransactionType(rawValue: type) { + return HotlineTransaction(type: transactionType, flags: flags, isReply: isReply, id: id, errorCode: errorCode, totalSize: transactionSize, dataSize: dataSize) + } } 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: completed handshake") + self.sendLogin() + self.receiveTransaction() + } + }) + } + + func sendLogin(callback: (() -> Void)? = nil) { + DispatchQueue.main.async { + self.connectionStatus = .loggingIn + } + + var t = HotlineTransaction(type: .login) + t.setParameterEncodedString(type: .userLogin, val: "") + t.setParameterEncodedString(type: .userPassword, val: "") + t.setParameterUInt32(type: .userIconID, val: self.userIconID) + t.setParameterString(type: .userName, val: self.userName) + t.setParameterUInt32(type: .versionNumber, val: 151) + + print("HotlineClient: logging in...") + self.sendTransaction(t) { [weak self] in + print("HotlineClient: logged in!") + DispatchQueue.main.async { + self?.connectionStatus = .loggedIn + } + + callback?() + } + } + + func sendAgree(callback: (() -> Void)? = nil) { + var t = HotlineTransaction(type: .agreed) + t.setParameterString(type: .userName, val: self.userName) + t.setParameterUInt32(type: .userIconID, val: self.userIconID) + t.setParameterUInt32(type: .options, val: 0) + + print("HotlineClient: agreeing") + self.sendTransaction(t, callback: callback) + } + + func sendChat(message: String, callback: (() -> Void)? = nil) { + var t = HotlineTransaction(type: .sendChat) + t.setParameterString(type: .data, val: message) + + print("HotlineClient: sending chat...") + self.sendTransaction(t, callback: callback) + } + + func sendGetUserList(callback: (() -> Void)? = nil) { + let t = HotlineTransaction(type: .getUserNameList) + print("HotlineClient: fetching user list...") + self.sendTransaction(t, callback: callback) + } + + // MARK: - Incoming + + private func processTransaction(_ transaction: HotlineTransaction) { + switch(transaction.type) { + case .reply: + print("HotlineClient: GOT REPLY TRANSACTION? \(transaction)") + case .chatMessage: + print("HotlineClient: CHAT MESSAGE!") + if + let chatTextParam = transaction.getParameter(type: .data), + let chatText = chatTextParam.getString(), + let userNameParam = transaction.getParameter(type: .userName), + let userName = userNameParam.getString(), + let userIDParam = transaction.getParameter(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.getParameterList(type: .userInfo) + for u in userList { + let userInfo = u.getUserInfo() + print("HotlineClient: user \(userInfo.userName)") + } + case .notifyOfUserChange: + print("HotlineClient: user changed") + if let p = transaction.getParameter(type: .userName), + let userName = p.getString() { + print("HotlineClient: user name \(userName)") + } + case .disconnectMessage: + print("HotlineClient: DISCONNECTED BY SERVER!") + self.disconnect() + case .showAgreement: + if let agreementParam = transaction.getParameter(type: .data) { + if let agreementText = agreementParam.getString() { + print("AGREEMENT:", agreementText) + DispatchQueue.main.async { + self.agreement = agreementText + } + self.sendAgree() { +// self.sendGetUserList() + } + } + } + case .userAccess: + print("HotlineClient: user access transaction.") + default: + print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.parameters.count) parameters") + print(transaction.parameters) + } + } } |