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 | |
| parent | 2bdcc0c3d751d7c7626d8b0e9f79331b803c668b (diff) | |
Further work on protocol.
| -rw-r--r-- | Hotline/Network/HotlineClient.swift | 364 | ||||
| -rw-r--r-- | Hotline/Network/HotlineProtocol.swift | 212 | ||||
| -rw-r--r-- | Hotline/Utility/DataExtensions.swift | 15 |
3 files changed, 435 insertions, 156 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) + } + } } diff --git a/Hotline/Network/HotlineProtocol.swift b/Hotline/Network/HotlineProtocol.swift index 787675e..d5fe734 100644 --- a/Hotline/Network/HotlineProtocol.swift +++ b/Hotline/Network/HotlineProtocol.swift @@ -17,25 +17,145 @@ struct HotlineServer: Identifiable, Hashable { } } -struct HotlineTransactionParameter { +struct HotlineUser: Identifiable, Hashable { let id: UInt16 + let userName: String + + static func == (lhs: HotlineUser, rhs: HotlineUser) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} + +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 HotlineTransactionParameter { + let type: HotlineTransactionFieldType let dataSize: UInt16 let data: Data + + func getUInt8() -> UInt8? { + return data.readUInt8(at: 0) + } + + func getUInt16() -> UInt16? { + return data.readUInt16(at: 0) + } + + func getUInt32() -> UInt32? { + return data.readUInt32(at: 0) + } + + 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 + } - let flags: UInt8 - let isReply: UInt8 - let type: HotlineTransactionType - let id: UInt32 - let errorCode: UInt32 - let totalSize: UInt32 - let dataSize: UInt32 + 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 parameterCount: UInt16 = 0 - var parameters: [HotlineTransactionParameter]? = nil + var parameters: [HotlineTransactionParameter] = [] + + 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 setParameterUInt8(type: HotlineTransactionFieldType, val: UInt8) { + self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val))) +// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val)) + } + + mutating func setParameterUInt16(type: HotlineTransactionFieldType, val: UInt16) { + self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val))) +// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val)) + } + + mutating func setParameterUInt32(type: HotlineTransactionFieldType, val: UInt32) { + self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val))) +// self.parameters[type] = HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val)) + } + + mutating func setParameterEncodedString(type: HotlineTransactionFieldType, val: String) { + let encodedVal = String(val.utf8.map { char in + Character(UnicodeScalar(0xFF - char)) + }) + + self.setParameterString(type: type, val: encodedVal) + } + + mutating func setParameterString(type: HotlineTransactionFieldType, val: String) { + var stringData = Data() +// stringData.appendUInt16(UInt16(val.count)) + stringData.append(contentsOf: val.utf8) + + self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(stringData.count), data: stringData)) +// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(stringData.count), data: stringData) + } + + func getParameter(type: HotlineTransactionFieldType) -> HotlineTransactionParameter? { + return self.parameters.first { p in + p.type == type + } + +// return self.parameters[type] + } + + func getParameterList(type: HotlineTransactionFieldType) -> [HotlineTransactionParameter] { + return self.parameters.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) @@ -43,45 +163,68 @@ struct HotlineTransaction { data.appendUInt16(self.type.rawValue) data.appendUInt32(self.id) data.appendUInt32(self.errorCode) - data.appendUInt32(self.totalSize) - data.appendUInt32(self.dataSize) - if let p = self.parameters, p.count > 0 { - data.appendUInt16(UInt16(p.count)) - for param in p { - data.appendUInt16(param.id) - data.appendUInt16(param.dataSize) - data.append(param.data) + if self.parameters.count > 0 { + var parameterData = Data() + parameterData.appendUInt16(UInt16(self.parameters.count)) + for param in self.parameters { + parameterData.appendUInt16(param.type.rawValue) + parameterData.appendUInt16(param.dataSize) + parameterData.append(param.data) } + + data.appendUInt32(UInt32(parameterData.count)) + data.appendUInt32(UInt32(parameterData.count)) + data.append(parameterData) + } + else { + data.appendUInt32(0) + data.appendUInt32(0) } } } +enum HotlineTransactionFieldType: UInt16 { + case userName = 102 // String + case userLogin = 105 // Encoded string + case userPassword = 106 // Encoded string + case userIconID = 104 // Integer + case userID = 103 // Integer + case data = 101 // String + case userAccess = 110 // 64-bit integer?? + case userFlags = 112 + case options = 113 // 32-bit integer? + case versionNumber = 160 // Integer + case bannerID = 161 + case serverName = 162 + case userInfo = 300 +} + enum HotlineTransactionType: UInt16 { - case unknown = 0 + case reply = 0 case error = 100 case getMessages = 101 - case newMessage = 102 + case newMessage = 102 // Server case oldPostNews = 103 - case serverMessage = 104 + case serverMessage = 104 // Server case sendChat = 105 - case chatMessage = 106 + case chatMessage = 106 // Server case login = 107 case sendInstantMessage = 108 - case showAgreement = 109 + case showAgreement = 109 // Server case disconnectUser = 110 - case disconnectMessage = 111 + case disconnectMessage = 111 // Server case inviteToNewChat = 112 - case inviteToChat = 113 + case inviteToChat = 113 // Server case rejectChatInvite = 114 case joinChat = 115 case leaveChat = 116 - case notifyChatOfUserChange = 117 - case notifyChatOfUserDelete = 118 - case notifyChatSubject = 119 + case notifyChatOfUserChange = 117 // Server + case notifyChatOfUserDelete = 118 // Server + case notifyChatSubject = 119 // Server case setChatSubject = 120 case agreed = 121 - case serverBanner = 122 + case serverBanner = 122 // Server case getFileNameList = 200 case downloadFile = 202 case uploadFile = 203 @@ -92,20 +235,20 @@ enum HotlineTransactionType: UInt16 { case moveFile = 208 case makeFileAlias = 209 case downloadFolder = 210 - case downloadInfo = 211 + case downloadInfo = 211 // Server case downloadBanner = 212 case uploadFolder = 213 case getUserNameList = 300 - case notifyOfUserChange = 301 - case notifyOfUserDelete = 302 + 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 - case userBroadcast = 355 + case userAccess = 354 // Server + case userBroadcast = 355 // Client & Server case getNewsCategoryNameList = 370 case getNewsArticleNameList = 371 case deleteNewsItem = 380 @@ -114,5 +257,6 @@ enum HotlineTransactionType: UInt16 { case getNewsArticleData = 400 case postNewsArticle = 410 case deleteNewsArticle = 411 + case connectionKeepAlive = 500 } diff --git a/Hotline/Utility/DataExtensions.swift b/Hotline/Utility/DataExtensions.swift index 3f7101b..caf26a9 100644 --- a/Hotline/Utility/DataExtensions.swift +++ b/Hotline/Utility/DataExtensions.swift @@ -6,6 +6,21 @@ enum Endianness { } extension Data { + init(_ val: UInt8) { + self.init() + self.appendUInt8(val) + } + + init(_ val: UInt16) { + self.init() + self.appendUInt16(val) + } + + init(_ val: UInt32) { + self.init() + self.appendUInt32(val) + } + func readUInt8(at offset: Int) -> UInt8? { guard offset >= 0, offset + MemoryLayout<UInt8>.size <= self.count else { return nil |