diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-01-13 19:12:12 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-01-13 19:12:12 -0800 |
| commit | 3f4e2c33a4c2719388d9378def981688fcaed210 (patch) | |
| tree | 01928a9347ca2dc18478da4107365a968637ccf5 /Hotline | |
| parent | 2f210991684c87073dd7534d2d0416d91cf50275 (diff) | |
Rewrite HotlineClient to use new simpler NetSocket stream based socket lib. Seems to be working well and has fixed some connection issues with certain servers. This does away with Apple's NWConnection for everything but file transfers which I still want to be processing on another thread so will keep NWConnection for now.
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 966 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineProtocol.swift | 119 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineTrackerClient.swift | 22 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 111 | ||||
| -rw-r--r-- | Hotline/Shared/NetSocket.swift | 58 | ||||
| -rw-r--r-- | Hotline/Utility/FoundationExtensions.swift | 71 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 1 |
7 files changed, 583 insertions, 765 deletions
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index ccb0c4c..0535f7b 100644 --- a/Hotline/Hotline/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -22,6 +22,14 @@ struct HotlineTransactionInfo { let reply: ((HotlineTransaction) -> Void)? } +private struct HotlineLogin { + let login: String? + let password: String? + let username: String + let iconID: UInt16 + let callback: ((HotlineTransactionError?, String?, UInt16?) -> Void)? +} + protocol HotlineClientDelegate: AnyObject { func hotlineGetUserInfo() -> (String, UInt16) func hotlineStatusChanged(status: HotlineClientStatus) @@ -45,7 +53,13 @@ extension HotlineClientDelegate { func hotlineUserDisconnected(userID: UInt16) {} } -class HotlineClient { +enum HotlineClientStage { + case handshake + case packetHeader + case packetBody +} + +class HotlineClient: NetSocketDelegate { static let handshakePacket = Data([ 0x54, 0x52, 0x54, 0x50, // 'TRTP' protocol ID 0x48, 0x4F, 0x54, 0x4C, // Sub-protocol ID @@ -53,6 +67,13 @@ class HotlineClient { 0x00, 0x02, // Sub-version ]) + static let HandshakePacket: [UInt8] = [ + 0x54, 0x52, 0x54, 0x50, // 'TRTP' protocol ID + 0x48, 0x4F, 0x54, 0x4C, // Sub-protocol ID + 0x00, 0x01, // Version + 0x00, 0x02, // Sub-version + ] + weak var delegate: HotlineClientDelegate? var connectionStatus: HotlineClientStatus = .disconnected @@ -61,325 +82,289 @@ class HotlineClient { private var serverAddress: String? = nil private var serverPort: UInt16? = nil - private var connection: NWConnection? +// private var connection: NWConnection? private var transactionLog: [UInt32:(HotlineTransactionType, ((HotlineTransaction, HotlineTransactionError?) -> Void)?)] = [:] - init() { - // let downloadsPath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask) - // print("DOWNLOAD TO: \(downloadsPath)") + private var socket: NetSocket? + private var stage: HotlineClientStage = .handshake + private var packet: HotlineTransaction? = nil + private var loginDetails: HotlineLogin? = nil + + init() {} + + // MARK: - NetSocket Delegate + + @MainActor func netsocketConnected(socket: NetSocket) { + self.updateConnectionStatus(.loggingIn) + self.stage = .handshake } - // MARK: - + @MainActor func netsocketDisconnected(socket: NetSocket, error: Error?) { + self.updateConnectionStatus(.disconnected) + self.stage = .handshake + } - func login(_ address: String, port: UInt16, login: String?, password: String?, username: String, iconID: UInt16, callback: ((HotlineTransactionError?, String?, UInt16?) -> Void)?) { - print("AWAITING CONNECT") - self.connect(address: address, port: port) { [weak self] success in - guard success else { - DispatchQueue.main.async { - callback?(.networkFailure, nil, nil) - } - return - } - - print("AWAITING HANDSHAKE") - self?.sendHandshake() { [weak self] success in - guard success else { - DispatchQueue.main.async { - callback?(.networkFailure, nil, nil) - } - return - } - - print("AWAITING LOGIN \(login ?? "empty login") \(password ?? "empty pass")") - self?.sendLogin(login: login ?? "", password: password ?? "", username: username, iconID: iconID) { err, serverName, serverVersion in -// guard err == nil else { -// DispatchQueue.main.async { -// callback?(err, nil, nil) -// } -// return -// } - - print("LOGGED INTO SERVER: \(String(describing: serverName?.debugDescription)) \(serverVersion.debugDescription)") - - DispatchQueue.main.async { - callback?(err, serverName, serverVersion) - } - - } - } + @MainActor func netsocketReceived(socket: NetSocket, bytes: [UInt8]) { + switch self.stage { + case .handshake: + self.receiveHandshake() + case .packetHeader: + self.receivePacket() + case .packetBody: + self.receivePacket() } } - private func connect(address: String, port: UInt16, callback: ((Bool) -> Void)?) { - self.serverAddress = address - self.serverPort = port - - let tcpOptions = NWProtocolTCP.Options() - tcpOptions.enableKeepalive = true -// tcpOptions.enableFastOpen = true -// tcpOptions.keepaliveInterval = 30 -// tcpOptions.connectionTimeout = 30 - let connectionParameters: NWParameters - connectionParameters = NWParameters(tls: nil, tcp: tcpOptions) + // MARK: - Connect + + @MainActor func login(address: String, port: Int, login: String?, password: String?, username: String, iconID: UInt16, callback: ((HotlineTransactionError?, String?, UInt16?) -> Void)?) { + if self.socket != nil { + self.socket?.delegate = nil + self.socket?.close() + self.socket = nil + } + self.packet = nil - self.connectCallback = callback + self.loginDetails = HotlineLogin(login: login, password: password, username: username, iconID: iconID, callback: callback) - self.connection = NWConnection(host: NWEndpoint.Host(address), port: NWEndpoint.Port(rawValue: port)!, using: connectionParameters) - self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in - guard let self = self else { - return - } - - switch newState { - case .ready: - print("HotlineClient: connection ready!") - self.updateConnectionStatus(.connected) - self.connectCallback?(true) - self.connectCallback = nil -// case .waiting(let err): -// print("HotlineClient: connection waiting \(err)...") -// switch err { -// case .posix(let errCode): -// print("HotlineClient: posix error code \(errCode)") -// self.disconnect() -// case .tls(let errStatus): -// print("HotlineClient: tls error code \(errStatus)") -// self.disconnect() -// case .dns(let errType): -// print("HotlineClient: DNS Error code \(errType)") -// self.disconnect() -// default: -// print("HotlineClient: error code \(err)") -// } - case .cancelled: - print("HotlineClient: connection cancelled") - self.updateConnectionStatus(.disconnected) - self.reset() - self.connectCallback?(false) - self.connectCallback = nil - case .failed(let err): - print("HotlineClient: connection error \(err)") - self.updateConnectionStatus(.disconnected) - self.reset() - self.connectCallback?(false) - self.connectCallback = nil - default: - print("HotlineClient: hmm", newState) - break - } - } + self.socket = NetSocket() + self.socket?.delegate = self self.updateConnectionStatus(.connecting) - self.connection?.start(queue: .global()) + self.socket?.connect(host: address, port: port) + self.socket?.write(HotlineClient.HandshakePacket) } - private func reset() { - self.transactionLog = [:] + @MainActor func receiveHandshake() { + guard let socket = self.socket, + self.stage == .handshake, + socket.available >= 8 else { + return + } + + var handshake: [UInt8] = socket.read(count: 8) + + // Verify handshake data + guard let protocolID = handshake.consumeUInt32(), + protocolID == "TRTP".fourCharCode() else { + // TODO: Close with appropriate error + socket.close() + return + } + + // Check for error code + guard let errorCode = handshake.consumeUInt32(), + errorCode == 0 else { + // TODO: Close with wrapped error + socket.close() + return + } + + self.stage = .packetHeader + + let session = self.loginDetails! + self.loginDetails = nil + self.sendLogin(login: session.login ?? "", password: session.password ?? "", username: session.username, iconID: session.iconID) { err, serverName, serverVersion in + session.callback?(err, serverName, serverVersion) + } + + self.receivePacket() } - func disconnect() { - for (_, replyInfo) in self.transactionLog { - let replyCallback = replyInfo.1 - replyCallback?(HotlineTransaction(type: replyInfo.0), .networkFailure) - } + @MainActor func disconnect() { self.transactionLog = [:] - self.connection?.cancel() - self.connection = nil + self.packet = nil + + self.socket?.close() + self.socket?.delegate = nil + self.socket = nil } - // MARK: - + // MARK: - Packets - private func sendTransaction(_ t: HotlineTransaction, sent sentCallback: ((Bool) -> Void)? = nil, reply replyCallback: ((HotlineTransaction, HotlineTransactionError?) -> Void)? = nil) { - guard let c = connection else { - sentCallback?(false) + @MainActor private func sendPacket(_ t: HotlineTransaction, callback: ((HotlineTransaction, HotlineTransactionError?) -> Void)? = nil) { + guard let socket = self.socket else { return } print("HotlineClient => \(t.id) \(t.type)") - if replyCallback != nil { - self.transactionLog[t.id] = (t.type, replyCallback) + if callback != nil { + self.transactionLog[t.id] = (t.type, callback) } - c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in - if error != nil { - sentCallback?(false) - self?.transactionLog[t.id] = nil - self?.disconnect() - return - } - - sentCallback?(true) - }) + socket.write(t.encoded()) } - private func sendTransaction(_ t: HotlineTransaction, sent sentCallback: ((Bool) -> Void)? = nil) { - self.sendTransaction(t, sent: sentCallback, reply: nil) + @MainActor private func receivePacket() { + guard let socket = self.socket else { + return + } + + var done: Bool = false + repeat { + switch self.stage { + case .packetHeader: + guard socket.has(HotlineTransaction.headerSize) else { + done = true + break + } + + let headerData: [UInt8] = socket.read(count: HotlineTransaction.headerSize) + guard let packet = HotlineTransaction(from: headerData) else { + done = true + break + } + + self.packet = packet + if packet.dataSize == 0 { + self.stage = .packetHeader + self.processPacket() + } + else { + self.stage = .packetBody + } + + case .packetBody: + guard let packet = self.packet, socket.has(Int(packet.dataSize)) else { + done = true + break + } + + let bodyData: [UInt8] = socket.read(count: Int(packet.dataSize)) + self.packet?.decodeFields(from: bodyData) + self.stage = .packetHeader + self.processPacket() + + default: + done = true + break + } + } while !done } - private func receiveTransaction() { - guard let c = connection else { - print("HotlineClient: no connection for transaction.") + @MainActor private func processPacket() { + guard let packet = self.packet else { 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 packet.type == .reply || packet.isReply == 1 { + print("HotlineClient <= \(packet.type) to \(packet.id):") + } + else { + print("HotlineClient <= \(packet.type) \(packet.id)") + } + + if packet.isReply == 1 || packet.type == .reply { + self.processReplyPacket() + return + } + + // Mark packet is processed + self.packet = nil + + switch(packet.type) { + case .chatMessage: + if + let chatTextParam = packet.getField(type: .data), + let chatText = chatTextParam.getString() + { + print("HotlineClient: \(chatText)") + self.delegate?.hotlineReceivedChatMessage(message: chatText) } - if let error = error { - print("HotlineClient: transaction error \(error)") - self.disconnect() - return + case .notifyOfUserChange: + if let usernameField = packet.getField(type: .userName), + let username = usernameField.getString(), + let userIDField = packet.getField(type: .userID), + let userID = userIDField.getUInt16(), + let userIconIDField = packet.getField(type: .userIconID), + let userIconID = userIconIDField.getUInt16(), + let userFlagsField = packet.getField(type: .userFlags), + let userFlags = userFlagsField.getUInt16() { + print("HotlineClient: User changed \(username) icon: \(userIconID)") + + let user = HotlineUser(id: userID, iconID: userIconID, status: userFlags, name: username) + self.delegate?.hotlineUserChanged(user: user) } - - guard let headerData = headerData, !headerData.isEmpty else { - self.receiveTransaction() - return + case .notifyOfUserDelete: + if let userIDField = packet.getField(type: .userID), + let userID = userIDField.getUInt16() { + self.delegate?.hotlineUserDisconnected(userID: userID) } - // print("HotlineClient: received \(headerData.count) header bytes") + case .disconnectMessage: + // Server disconnected us. + print("HotlineClient ❌") + self.disconnect() + + case .serverMessage: + if let messageField = packet.getField(type: .data), + let message = messageField.getString() { + self.delegate?.hotlineReceivedServerMessage(message: message) + } - 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() + case .showAgreement: + if let _ = packet.getField(type: .noServerAgreement) { + // Server told us there is no agreement to show. + return + } + if let agreementParam = packet.getField(type: .data) { + if let agreementText = agreementParam.getString() { + self.delegate?.hotlineReceivedAgreement(text: agreementText) } } - else { - // Here we failed to parse the current transaction. - // We should consider disconnecting perhaps. - // But for now we'll continue receiving. - self.receiveTransaction() + + case .userAccess: + print("HotlineClient: user access info \(packet.getField(type: .userAccess).debugDescription)") + if let accessParam = packet.getField(type: .userAccess) { + if let accessValue = accessParam.getUInt64() { + let accessOptions = HotlineUserAccessOptions(rawValue: accessValue) + self.delegate?.hotlineReceivedUserAccess(options: accessOptions) + } } + + default: + print("HotlineClient: UNKNOWN transaction \(packet.type) with \(packet.fields.count) parameters") + print(packet.fields) } } - 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") - } + @MainActor private func processReplyPacket() { + guard let packet = self.packet else { + return } - return nil - } - - // MARK: - Messages - - private func sendHandshake(callback: ((Bool) -> Void)?) { - guard let c = self.connection else { - print("HotlineClient: invalid connection to send handshake.") + if packet.errorCode != 0 { + let errorField: HotlineTransactionField? = packet.getField(type: .errorText) + print("HotlineClient 😵 \(packet.errorCode): \(errorField?.getString() ?? "")") + } + + guard let replyCallbackInfo = self.transactionLog[packet.id] else { + print("Hmm, no reply waiting though") return } - c.send(content: HotlineClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in - if let err = error { - print("HotlineClient: sending handshake failed \(err)") - callback?(false) - 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() - callback?(false) - return - } - - let protocolID = data.readUInt32(at: 0)! - if protocolID != "TRTP".fourCharCode() { - print("HotlineClient: invalid handshake protocol ID \(protocolID)") - self.disconnect() - callback?(false) - return - } - - let errorCode = data.readUInt32(at: 4)! - if errorCode != 0 { // 0 == no error - print("HotlineClient: handshake error", errorCode) - self.disconnect() - callback?(false) - return - } - - callback?(true) - print("HotlineClient 🤝") - self.receiveTransaction() - } - }) + self.transactionLog[packet.id] = nil + + print("HotlineClient reply in response to \(replyCallbackInfo.0)") + + let replyCallback = replyCallbackInfo.1 + + guard packet.errorCode == 0 else { + let errorField: HotlineTransactionField? = packet.getField(type: .errorText) + print("HotlineClient 😵 \(packet.errorCode): \(errorField?.getString() ?? "")") + replyCallback?(packet, .error(packet.errorCode, errorField?.getString())) + return + } + + replyCallback?(packet, nil) } - func sendLogin(login: String, password: String, username: String, iconID: UInt16, callback: ((HotlineTransactionError?, String?, UInt16?) -> Void)?) { - self.updateConnectionStatus(.loggingIn) - + // MARK: - Messages + + @MainActor func sendLogin(login: String, password: String, username: String, iconID: UInt16, callback: ((HotlineTransactionError?, String?, UInt16?) -> Void)?) { var t = HotlineTransaction(type: .login) t.setFieldEncodedString(type: .userLogin, val: login) t.setFieldEncodedString(type: .userPassword, val: password) @@ -387,40 +372,31 @@ class HotlineClient { t.setFieldString(type: .userName, val: username) t.setFieldUInt32(type: .versionNumber, val: 123) - self.sendTransaction(t) { success in - if !success { - DispatchQueue.main.async { - callback?(.networkFailure, nil, nil) - } - } - } reply: { [weak self] replyTransaction, err in - print("GOT LOGIN REPLY") + self.sendPacket(t) { [weak self] reply, err in self?.updateConnectionStatus(.loggedIn) var serverVersion: UInt16? var serverName: String? if - let serverVersionField = replyTransaction.getField(type: .versionNumber), + let serverVersionField = reply.getField(type: .versionNumber), let serverVersionValue = serverVersionField.getUInt16() { serverVersion = serverVersionValue print("SERVER VERSION: \(serverVersionValue)") } if - let serverNameField = replyTransaction.getField(type: .serverName), + let serverNameField = reply.getField(type: .serverName), let serverNameValue = serverNameField.getString() { serverName = serverNameValue print("SERVER NAME: \(serverNameValue)") } - DispatchQueue.main.async { - callback?(err, serverName, serverVersion) - } + callback?(err, serverName, serverVersion) } } - func sendSetClientUserInfo(username: String, iconID: UInt16, options: HotlineUserOptions = [], autoresponse: String? = nil, sent: ((Bool) -> Void)? = nil) { + @MainActor func sendSetClientUserInfo(username: String, iconID: UInt16, options: HotlineUserOptions = [], autoresponse: String? = nil) { var t = HotlineTransaction(type: .setClientUserInfo) t.setFieldString(type: .userName, val: username) t.setFieldUInt16(type: .userIconID, val: iconID) @@ -429,131 +405,103 @@ class HotlineClient { t.setFieldString(type: .automaticResponse, val: text) } - self.sendTransaction(t, sent: sent) + self.sendPacket(t) } - func sendAgree(username: String, iconID: UInt16, options: HotlineUserOptions, sent: ((Bool) -> Void)? = nil) { + @MainActor func sendAgree(username: String, iconID: UInt16, options: HotlineUserOptions) { let t = HotlineTransaction(type: .agreed) // t.setFieldString(type: .userName, val: username) // t.setFieldUInt16(type: .userIconID, val: iconID) // t.setFieldUInt8(type: .options, val: options.rawValue) - self.sendTransaction(t, sent: sent) + self.sendPacket(t) } - func sendChat(message: String, encoding: String.Encoding = .utf8, sent sentCallback: ((Bool) -> Void)?) { + @MainActor func sendChat(message: String, encoding: String.Encoding = .utf8) { var t = HotlineTransaction(type: .sendChat) t.setFieldString(type: .data, val: message, encoding: encoding) - self.sendTransaction(t, sent: sentCallback) + self.sendPacket(t) } - func sendGetUserList(sent sentCallback: ((Bool) -> Void)? = nil) { - print("SENDING GET USER LIST") + @MainActor func sendGetUserList() { let t = HotlineTransaction(type: .getUserNameList) - self.sendTransaction(t, sent: sentCallback) { [weak self] replyTransaction, err in - print("GOT USER LIST") + self.sendPacket(t) { [weak self] reply, err in var newUsers: [UInt16:HotlineUser] = [:] var newUserList: [HotlineUser] = [] - for u in replyTransaction.getFieldList(type: .userNameWithInfo) { + for u in reply.getFieldList(type: .userNameWithInfo) { let user = u.getUser() newUsers[user.id] = user newUserList.append(user) } - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineReceivedUserList(users: newUserList) - } + self?.delegate?.hotlineReceivedUserList(users: newUserList) } } - func sendGetMessageBoard(callback: ((HotlineTransactionError?, [String]) -> Void)?) { + @MainActor func sendGetMessageBoard(callback: ((HotlineTransactionError?, [String]) -> Void)?) { let t = HotlineTransaction(type: .getMessageBoard) - self.sendTransaction(t) { success in - if !success { - DispatchQueue.main.async { - callback?(.networkFailure, []) - } - } - } reply: { replyTransaction, err in - if err != nil { - DispatchQueue.main.async { - callback?(err, []) - } + self.sendPacket(t) { reply, err in + guard err == nil, + let textField = reply.getField(type: .data), + let text = textField.getString() else { + callback?(err, []) return } - - if let textField = replyTransaction.getField(type: .data), - let text = textField.getString() { - var messages: [String] = [] - let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/ - let matches = text.matches(of: messageBoardRegex) - var start = text.startIndex - - if matches.count > 0 { - for match in matches { - let range = match.range - messages.append(String(text[start..<range.lowerBound])) - start = range.upperBound - } - } - else { - messages.append(text) - } - - DispatchQueue.main.async { - callback?(err, messages) + + var messages: [String] = [] + let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/ + let matches = text.matches(of: messageBoardRegex) + var start = text.startIndex + + if matches.count > 0 { + for match in matches { + let range = match.range + messages.append(String(text[start..<range.lowerBound])) + start = range.upperBound } - - - -// continuation.resume(returning: messages) -// DispatchQueue.main.async { -// self.messageBoardMessages = messages -// } } + else { + messages.append(text) + } + + callback?(err, messages) } } - func sendGetNewsCategories(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsCategory]) -> Void)?) { + @MainActor func sendGetNewsCategories(path: [String] = [], callback: (([HotlineNewsCategory]) -> Void)?) { var t = HotlineTransaction(type: .getNewsCategoryNameList) if !path.isEmpty { t.setFieldPath(type: .newsPath, val: path) } - self.sendTransaction(t, sent: sent, reply: { rt, err in + self.sendPacket(t) { reply, err in var categories: [HotlineNewsCategory] = [] - for categoryListItem in rt.getFieldList(type: .newsCategoryListData15) { + for categoryListItem in reply.getFieldList(type: .newsCategoryListData15) { var c = categoryListItem.getNewsCategory() c.path = path + [c.name] categories.append(c) - print("CATEGORY: \(c)") - } - DispatchQueue.main.async { - reply?(categories) } - }) + callback?(categories) + } } - func sendGetNewsArticle(id articleID: UInt32, path: [String], flavor: String, sent: ((Bool) -> Void)? = nil, reply: ((String?) -> Void)? = nil) { + @MainActor func sendGetNewsArticle(id articleID: UInt32, path: [String], flavor: String, callback: ((String?) -> Void)? = nil) { var t = HotlineTransaction(type: .getNewsArticleData) t.setFieldPath(type: .newsPath, val: path) t.setFieldUInt32(type: .newsArticleID, val: articleID) t.setFieldString(type: .newsArticleDataFlavor, val: flavor, encoding: .ascii) - self.sendTransaction(t, sent: sent, reply: { r, err in - if err != nil { - reply?(nil) + self.sendPacket(t) { reply, err in + guard err == nil, + let articleData = reply.getField(type: .newsArticleData), + let articleString = articleData.getString() else { + callback?(nil) return } - let articleData = r.getField(type: .newsArticleData) - let articleString = articleData?.getString() - - DispatchQueue.main.async { - reply?(articleString) - } - }) + callback?(articleString) + } } - func postNewsArticle(title: String, text: String, path: [String] = [], parentID: UInt32? = nil, sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsArticle]) -> Void)? = nil) { + @MainActor func postNewsArticle(title: String, text: String, path: [String] = [], parentID: UInt32? = nil, callback: (([HotlineNewsArticle]) -> Void)? = nil) { var t = HotlineTransaction(type: .postNewsArticle) if !path.isEmpty { t.setFieldPath(type: .newsPath, val: path) @@ -566,94 +514,73 @@ class HotlineClient { t.setFieldUInt32(type: .newsArticleFlags, val: 0) t.setFieldString(type: .newsArticleData, val: text) - self.sendTransaction(t, sent: sent, reply: { r, err in - if err != nil { - reply?([]) + self.sendPacket(t) { reply, err in + guard err == nil, + let articleData = reply.getField(type: .newsArticleListData) else { + callback?([]) return } var articles: [HotlineNewsArticle] = [] -// let articleData = r.getField(type: .newsArticleListData) - -// print("ARTICLE DATA?", articleData) - - if let articleData = r.getField(type: .newsArticleListData) { - let newsList = articleData.getNewsList() - for art in newsList.articles { - var blah = art - blah.path = path - articles.append(blah) - - print(blah.title) - } + let newsList = articleData.getNewsList() + for art in newsList.articles { + var blah = art + blah.path = path + articles.append(blah) } - DispatchQueue.main.async { - reply?(articles) - } - }) + callback?(articles) + } } - func sendGetNewsArticles(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsArticle]) -> Void)? = nil) { + @MainActor func sendGetNewsArticles(path: [String] = [], callback: (([HotlineNewsArticle]) -> Void)? = nil) { var t = HotlineTransaction(type: .getNewsArticleNameList) if !path.isEmpty { t.setFieldPath(type: .newsPath, val: path) } - self.sendTransaction(t, sent: sent, reply: { r, err in - if err != nil { - reply?([]) + self.sendPacket(t) { reply, err in + guard err == nil, + let articleData = reply.getField(type: .newsArticleListData) else { + callback?([]) return } var articles: [HotlineNewsArticle] = [] -// let articleData = r.getField(type: .newsArticleListData) - -// print("ARTICLE DATA?", articleData) - - if let articleData = r.getField(type: .newsArticleListData) { - let newsList = articleData.getNewsList() - for art in newsList.articles { - var blah = art - blah.path = path - articles.append(blah) - - print(blah.title) - } - } - - DispatchQueue.main.async { - reply?(articles) + let newsList = articleData.getNewsList() + for art in newsList.articles { + var blah = art + blah.path = path + articles.append(blah) } - }) + + callback?(articles) + } } - func sendGetFileList(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineFile]) -> Void)? = nil) { + @MainActor func sendGetFileList(path: [String] = [], callback: (([HotlineFile]) -> Void)? = nil) { var t = HotlineTransaction(type: .getFileNameList) - if !path.isEmpty { t.setFieldPath(type: .filePath, val: path) } - self.sendTransaction(t, sent: sent, reply: { r, err in - if err != nil { - reply?([]) + self.sendPacket(t) { reply, err in + guard err == nil else { + callback?([]) return } var files: [HotlineFile] = [] - for fi in r.getFieldList(type: .fileNameWithInfo) { + for fi in reply.getFieldList(type: .fileNameWithInfo) { let file = fi.getFile() file.path = path + [file.name] files.append(file) } - DispatchQueue.main.async { - reply?(files) - } - }) + callback?(files) + } } - func sendDownloadFile(name fileName: String, path filePath: [String], preview: Bool = false, sent: ((Bool) -> Void)? = nil, reply: ((Bool, UInt32?, Int?, Int?, Int?) -> Void)? = nil) { + @MainActor func sendDownloadFile(name fileName: String, path filePath: [String], preview: Bool = false, callback: ((Bool, UInt32?, Int?, Int?, Int?) -> Void)? = nil) { var t = HotlineTransaction(type: .downloadFile) t.setFieldString(type: .fileName, val: fileName) t.setFieldPath(type: .filePath, val: filePath) @@ -661,223 +588,48 @@ class HotlineClient { t.setFieldUInt32(type: .fileTransferOptions, val: 2) } - print("DOWNLOAD \(fileName) AT PATH \(filePath)") - - self.sendTransaction(t, sent: sent, reply: { r, err in - if err != nil { - DispatchQueue.main.async { - reply?(false, nil, nil, nil, nil) - } + self.sendPacket(t) { reply, err in + guard err == nil, + let transferSizeField = reply.getField(type: .transferSize), + let transferSize = transferSizeField.getInteger(), + let transferReferenceField = reply.getField(type: .referenceNumber), + let referenceNumber = transferReferenceField.getUInt32(), + let transferFileSizeField = reply.getField(type: .fileSize), + let transferFileSize = transferFileSizeField.getInteger() else { + callback?(false, nil, nil, nil, nil) return } + + let transferWaitingCountField = reply.getField(type: .waitingCount) + let transferWaitingCount = transferWaitingCountField?.getInteger() - if let transferSizeField = r.getField(type: .transferSize), - let transferSize = transferSizeField.getInteger(), - let transferReferenceField = r.getField(type: .referenceNumber), - let referenceNumber = transferReferenceField.getUInt32(), - let transferFileSizeField = r.getField(type: .fileSize), - let transferFileSize = transferFileSizeField.getInteger() { - - let transferWaitingCountField = r.getField(type: .waitingCount) - let transferWaitingCount = transferWaitingCountField?.getInteger() - - DispatchQueue.main.async { - reply?(true, referenceNumber, transferSize, transferFileSize, transferWaitingCount) - } - } - else { - DispatchQueue.main.async { - reply?(false, nil, nil, nil, nil) - } - } - }) + callback?(true, referenceNumber, transferSize, transferFileSize, transferWaitingCount) + } } - func sendDownloadBanner(sent: ((Bool) -> Void)? = nil, reply: ((Bool, UInt32?, Int?) -> Void)? = nil) { + @MainActor func sendDownloadBanner(callback: ((Bool, UInt32?, Int?) -> Void)? = nil) { let t = HotlineTransaction(type: .downloadBanner) - self.sendTransaction(t, sent: sent, reply: { r, err in - if err != nil { - DispatchQueue.main.async { - reply?(false, nil, nil) - } + self.sendPacket(t) { reply, err in + guard err == nil, + let transferSizeField = reply.getField(type: .transferSize), + let transferSize = transferSizeField.getInteger(), + let transferReferenceField = reply.getField(type: .referenceNumber), + let referenceNumber = transferReferenceField.getUInt32() else { + callback?(false, nil, nil) return } - - if let transferSizeField = r.getField(type: .transferSize), - let transferSize = transferSizeField.getInteger(), - let transferReferenceField = r.getField(type: .referenceNumber), - let referenceNumber = transferReferenceField.getUInt32() { - - DispatchQueue.main.async { - reply?(true, referenceNumber, transferSize) - } - } - else { - DispatchQueue.main.async { - reply?(false, nil, nil) - } - } - }) - } - - // func sendGetNews(callback: (() -> Void)? = nil) { - // let t = HotlineTransaction(type: .getNewsFile) - // self.sendTransaction(t, callback: callback) - // } - - // MARK: - Incoming - - private func processReply(_ transaction: HotlineTransaction) { - if transaction.errorCode != 0 { - let errorField: HotlineTransactionField? = transaction.getField(type: .errorText) - print("HotlineClient 😵 \(transaction.errorCode): \(errorField?.getString() ?? "")") - } - - guard let replyCallbackInfo = self.transactionLog[transaction.id] else { - return - } - - self.transactionLog[transaction.id] = nil - - print("HotlineClient reply in response to \(replyCallbackInfo.0)") - -// var replyError: HotlineTransactionError? = nil - -// defer { -// let replyCallback = replyCallbackInfo.1 -// DispatchQueue.main.async { -// replyCallback?(transaction, replyError) -// } -// } - - let replyCallback = replyCallbackInfo.1 - - guard transaction.errorCode == 0 else { - let errorField: HotlineTransactionField? = transaction.getField(type: .errorText) - - print("HotlineClient 😵 \(transaction.errorCode): \(errorField?.getString() ?? "")") - - DispatchQueue.main.async { - replyCallback?(transaction, .error(transaction.errorCode, errorField?.getString())) - } - return - } - - DispatchQueue.main.async { - replyCallback?(transaction, nil) - } - } - - private func processTransaction(_ transaction: HotlineTransaction) { - if transaction.type == .reply || transaction.isReply == 1 { - print("HotlineClient <= \(transaction.type) to \(transaction.id):") - print(transaction) - } - else { - print("HotlineClient <= \(transaction.type) \(transaction.id)") - } - - if transaction.isReply == 1 { - self.processReply(transaction) - return - } -// if self.transactionLog[transaction.id] != nil { -// self.processReply(transaction) -// return -// } - - switch(transaction.type) { - case .reply: - self.processReply(transaction) - - case .chatMessage: - print("HotlineClient: chat \(transaction)") - if - let chatTextParam = transaction.getField(type: .data), - let chatText = chatTextParam.getString() - { - print("HotlineClient: \(chatText)") - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineReceivedChatMessage(message: chatText) - } - } - - case .notifyOfUserChange: - if let usernameField = transaction.getField(type: .userName), - let username = usernameField.getString(), - let userIDField = transaction.getField(type: .userID), - let userID = userIDField.getUInt16(), - let userIconIDField = transaction.getField(type: .userIconID), - let userIconID = userIconIDField.getUInt16(), - let userFlagsField = transaction.getField(type: .userFlags), - let userFlags = userFlagsField.getUInt16() { - print("HotlineClient: User changed \(username) icon: \(userIconID)") - - let user = HotlineUser(id: userID, iconID: userIconID, status: userFlags, name: username) - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineUserChanged(user: user) - } - } - case .notifyOfUserDelete: - if let userIDField = transaction.getField(type: .userID), - let userID = userIDField.getUInt16() { - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineUserDisconnected(userID: userID) - } - } - - case .disconnectMessage: - // Server disconnected us. - print("HotlineClient ❌") - self.disconnect() - - case .serverMessage: - if let messageField = transaction.getField(type: .data), - let message = messageField.getString() { - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineReceivedServerMessage(message: message) - } - } - - case .showAgreement: - if let _ = transaction.getField(type: .noServerAgreement) { - // Server told us there is no agreement to show. - return - } - if let agreementParam = transaction.getField(type: .data) { - if let agreementText = agreementParam.getString() { - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineReceivedAgreement(text: agreementText) - } - } - } - - case .userAccess: - print("HotlineClient: user access info \(transaction.getField(type: .userAccess).debugDescription)") - if let accessParam = transaction.getField(type: .userAccess) { - if let accessValue = accessParam.getUInt64() { - let accessOptions = HotlineUserAccessOptions(rawValue: accessValue) - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineReceivedUserAccess(options: accessOptions) - } - } - } - - default: - print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.fields.count) parameters") - print(transaction.fields) + callback?(true, referenceNumber, transferSize) } } + // MARK: - Utility - private func updateConnectionStatus(_ status: HotlineClientStatus) { + @MainActor private func updateConnectionStatus(_ status: HotlineClientStatus) { self.connectionStatus = status - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineStatusChanged(status: status) - } + self.delegate?.hotlineStatusChanged(status: status) } } diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift index 2251763..5897fe3 100644 --- a/Hotline/Hotline/HotlineProtocol.swift +++ b/Hotline/Hotline/HotlineProtocol.swift @@ -102,7 +102,7 @@ struct HotlineNewsList: Identifiable { var articles: [HotlineNewsArticle] = [] - init(from data: Data) { + init(from data: [UInt8]) { self.id = data.readUInt32(at: 0)! self.count = data.readUInt32(at: 4)! @@ -207,7 +207,7 @@ struct HotlineNewsCategory: Identifiable, Hashable { self.name = name } - init(from data: Data) { + init(from data: [UInt8]) { self.type = data.readUInt16(at: 0)! if self.type == 2 { @@ -268,7 +268,7 @@ class HotlineFile: Identifiable, Hashable { } } - init(from data: Data) { + init(from data: [UInt8]) { let typeRaw = data.readUInt32(at: 0)! let creatorRaw = data.readUInt32(at: 4)! @@ -315,7 +315,7 @@ struct HotlineUser: Identifiable, Hashable { self.name = name } - init(from data: Data) { + init(from data: [UInt8]) { self.id = data.readUInt16(at: 0)! self.iconID = data.readUInt16(at: 2)! self.status = data.readUInt16(at: 4)! @@ -328,13 +328,8 @@ struct HotlineUser: Identifiable, Hashable { hasher.combine(self.id) } - func encoded() -> Data { - var data = Data() - self.encode(to: &data) - return data - } - - func encode(to data: inout Data) { + func encoded() -> [UInt8] { + var data: [UInt8] = [] data.appendUInt16(self.id) data.appendUInt16(self.iconID) data.appendUInt16(self.status) @@ -342,31 +337,52 @@ struct HotlineUser: Identifiable, Hashable { let userNameData = name.data(using: .ascii, allowLossyConversion: true)! data.appendUInt16(UInt16(userNameData.count)) - data.append(userNameData) + data.appendData(userNameData) + + return 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 HotlineTransactionField { let type: HotlineTransactionFieldType let dataSize: UInt16 - let data: Data + let data: [UInt8] - init(type: HotlineTransactionFieldType, dataSize: UInt16, data: Data) { + init(type: HotlineTransactionFieldType, dataSize: UInt16, data: [UInt8]) { 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)) + self.init(type: type, dataSize: UInt16(1), data: [val]) } init(type: HotlineTransactionFieldType, val: UInt16) { - self.init(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val)) + self.init(type: type, dataSize: UInt16(2), data: [UInt8](val)) } init(type: HotlineTransactionFieldType, val: UInt32) { - self.init(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val)) + self.init(type: type, dataSize: UInt16(4), data: [UInt8](val)) + } + + init(type: HotlineTransactionFieldType, val: UInt64) { + self.init(type: type, dataSize: UInt16(8), data: [UInt8](val)) } init(type: HotlineTransactionFieldType, string: String, encoding: String.Encoding = .ascii, encrypt: Bool = false) { @@ -384,7 +400,7 @@ struct HotlineTransactionField { stringData = Data() } - self.init(type: type, dataSize: UInt16(stringData!.count), data: stringData!) + self.init(type: type, dataSize: UInt16(stringData!.count), data: [UInt8](stringData!)) } init(type: HotlineTransactionFieldType, string: String, encrypt: Bool) { @@ -404,7 +420,7 @@ struct HotlineTransactionField { } init(type: HotlineTransactionFieldType, pathComponents: [String]) { - var pathData = Data() + var pathData: [UInt8] = [] pathData.appendUInt16(UInt16(pathComponents.count)) for name in pathComponents { @@ -416,7 +432,7 @@ struct HotlineTransactionField { } pathData.appendUInt8(UInt8(nameData!.count)) - pathData.append(nameData!) + pathData.appendData(nameData!) } self.init(type: type, dataSize: UInt16(pathData.count), data: pathData) @@ -508,6 +524,47 @@ struct HotlineTransaction { self.id = HotlineTransaction.nextID() } + init?(from data: [UInt8]) { + guard + 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 totalSize = data.readUInt32(at: 12), + let dataSize = data.readUInt32(at: 16) else { + + return nil + } + + self.init(type: HotlineTransactionType(rawValue: type) ?? .unknown, flags: flags, isReply: isReply, id: id, errorCode: errorCode, totalSize: totalSize, dataSize: dataSize) + } + + mutating func decodeFields(from data: [UInt8]) { + var fieldData = data + + guard fieldData.count > 0, + let fieldCount = fieldData.consumeUInt16(), + fieldCount > 0 else { + return + } + + for _ in 0..<fieldCount { + if + let fieldID = fieldData.consumeUInt16(), + let fieldSize = fieldData.consumeUInt16(), + let fieldRemainingData: [UInt8] = fieldData.consumeBytes(Int(fieldSize)) { + + if let fieldType = HotlineTransactionFieldType(rawValue: fieldID) { + self.fields.append(HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldRemainingData)) + } + else { + print("HotlineClient: UNKNOWN FIELD TYPE!", fieldID, fieldSize) + } + } + } + } + init(type: HotlineTransactionType, flags: UInt8, isReply: UInt8, id: UInt32, errorCode: UInt32, totalSize: UInt32, dataSize: UInt32) { self.type = type self.flags = flags @@ -554,37 +611,33 @@ struct HotlineTransaction { } } - func encoded() -> Data { - var data = Data() - self.encode(to: &data) - return data - } - - func encode(to data: inout Data) { - data.appendUInt8(self.flags) + func encoded() -> [UInt8] { + var data: [UInt8] = [] + data.appendUInt8(0) data.appendUInt8(self.isReply) - data.appendUInt16(self.type.rawValue) + data.appendUInt16(self.isReply == 1 ? HotlineTransactionType.reply.rawValue : self.type.rawValue) data.appendUInt32(self.id) data.appendUInt32(self.errorCode) if self.fields.count > 0 { - var fieldData = Data() + var fieldData: [UInt8] = [] fieldData.appendUInt16(UInt16(self.fields.count)) for f in self.fields { fieldData.appendUInt16(f.type.rawValue) fieldData.appendUInt16(f.dataSize) - fieldData.append(f.data) + fieldData.appendData(f.data) } data.appendUInt32(UInt32(fieldData.count)) data.appendUInt32(UInt32(fieldData.count)) - data.append(fieldData) + data.appendData(fieldData) } else { data.appendUInt32(2) data.appendUInt32(2) data.appendUInt16(0) } + return data } } @@ -705,6 +758,8 @@ enum HotlineTransactionType: UInt16 { case postNewsArticle = 410 case deleteNewsArticle = 411 case connectionKeepAlive = 500 + + case unknown = 15000 } // MARK: - Utilities diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift index bd5c937..d7653df 100644 --- a/Hotline/Hotline/HotlineTrackerClient.swift +++ b/Hotline/Hotline/HotlineTrackerClient.swift @@ -153,28 +153,26 @@ class HotlineTrackerClient { var foundServers: [HotlineServer] = [] for _ in 1...self.serverCount { - if + guard let ip_1 = bytes.consumeUInt8(), let ip_2 = bytes.consumeUInt8(), let ip_3 = bytes.consumeUInt8(), let ip_4 = bytes.consumeUInt8(), let port = bytes.consumeUInt16(), - let _ = bytes.consumeBytes(2), + bytes.consume(2), let userCount = bytes.consumeUInt16(), let serverName = bytes.consumePString(), - let serverDescription = bytes.consumePString() { - - // Ignore servers that are just used as dividers in the tracker listing. - let validName = try? trackerSeparatorRegex.prefixMatch(in: serverName) - if validName == nil { - let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: serverName, description: serverDescription) - foundServers.append(server) - } - } - else { + let serverDescription = bytes.consumePString() else { print("HotlineTrackerClient: Data isn't long enough for next server") break } + + // Ignore servers that are just used as dividers in the tracker listing. + let validName = try? trackerSeparatorRegex.prefixMatch(in: serverName) + if validName == nil { + let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: serverName, description: serverDescription) + foundServers.append(server) + } } self.servers = foundServers diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index 445d09e..bdba307 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -246,7 +246,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { self.username = username self.iconID = iconID - self.client.login(server.address, port: UInt16(server.port), login: server.login, password: server.password, username: username, iconID: UInt16(iconID)) { [weak self] err, serverName, serverVersion in + self.client.login(address: server.address, port: server.port, login: server.login, password: server.password, username: username, iconID: UInt16(iconID)) { [weak self] err, serverName, serverVersion in self?.serverVersion = serverVersion if serverName != nil { self?.serverName = serverName @@ -256,19 +256,15 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { } } - @MainActor func sendUserInfo(username: String, iconID: Int, options: HotlineUserOptions = [], autoresponse: String? = nil, callback: ((Bool) -> Void)? = nil) { + @MainActor func sendUserInfo(username: String, iconID: Int, options: HotlineUserOptions = [], autoresponse: String? = nil) { self.username = username self.iconID = iconID - self.client.sendSetClientUserInfo(username: username, iconID: UInt16(iconID), options: options, autoresponse: autoresponse) { success in - callback?(success) - } + self.client.sendSetClientUserInfo(username: username, iconID: UInt16(iconID), options: options, autoresponse: autoresponse) } - @MainActor func getUserList(callback: ((Bool) -> Void)? = nil) { - self.client.sendGetUserList() { success in - callback?(success) - } + @MainActor func getUserList() { + self.client.sendGetUserList() } @MainActor func disconnect() { @@ -281,7 +277,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { } @MainActor func sendChat(_ text: String) { - self.client.sendChat(message: text, sent: nil) + self.client.sendChat(message: text) } @MainActor func getMessageBoard() async -> [String] { @@ -298,12 +294,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { @MainActor func getFileList(path: [String] = []) async -> [FileInfo] { return await withCheckedContinuation { [weak self] continuation in - self?.client.sendGetFileList(path: path, sent: { success in - if !success { - continuation.resume(returning: []) - return - } - }, reply: { [weak self] files in + self?.client.sendGetFileList(path: path) { [weak self] files in let parentFile = self?.findFile(in: self?.files ?? [], at: path) var newFiles: [FileInfo] = [] @@ -322,18 +313,13 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { continuation.resume(returning: newFiles) } - }) + } } } @MainActor func getNewsArticle(id articleID: UInt, at path: [String], flavor: String) async -> String? { return await withCheckedContinuation { [weak self] continuation in - self?.client.sendGetNewsArticle(id: UInt32(articleID), path: path, flavor: flavor, sent: { success in - if !success { - continuation.resume(returning: nil) - return - } - }, reply: { articleText in + self?.client.sendGetNewsArticle(id: UInt32(articleID), path: path, flavor: flavor) { articleText in // let parentNews = self?.findNews(in: self?.news ?? [], at: path) // var newCategories: [NewsInfo] = [] @@ -349,7 +335,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { // } continuation.resume(returning: articleText) - }) + } } } @@ -367,12 +353,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { } if requestCategories { - self?.client.sendGetNewsCategories(path: path, sent: { success in - if !success { - continuation.resume(returning: []) - return - } - }, reply: { [weak self] categories in + self?.client.sendGetNewsCategories(path: path) { [weak self] categories in // let parentNews = self?.findNews(in: self?.news ?? [], at: path) var newCategories: [NewsInfo] = [] @@ -391,19 +372,10 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { continuation.resume(returning: newCategories) } - }) + } } else { - self?.client.sendGetNewsArticles(path: path, sent: { success in - if !success { - DispatchQueue.main.async { - continuation.resume(returning: []) - } - return - } - - print("GET NEWS ARTICLES FROM \(path)") - }, reply: { [weak self] articles in + self?.client.sendGetNewsArticles(path: path) { [weak self] articles in // let parentNews = self?.findNews(in: self?.news ?? [], at: path) print("GENERATING NEWS") @@ -425,21 +397,14 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { continuation.resume(returning: newArticles) } - }) + } } } } @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] { return await withCheckedContinuation { [weak self] continuation in - self?.client.sendGetNewsCategories(path: path, sent: { success in - if !success { - DispatchQueue.main.async { - continuation.resume(returning: []) - } - return - } - }, reply: { [weak self] categories in + self?.client.sendGetNewsCategories(path: path) { [weak self] categories in let parentNews = self?.findNews(in: self?.news ?? [], at: path) var newCategories: [NewsInfo] = [] @@ -457,24 +422,15 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { continuation.resume(returning: newCategories) } - }) + } } } @MainActor func getArticles(at path: [String]) async -> [NewsInfo] { return await withCheckedContinuation { [weak self] continuation in - self?.client.sendGetNewsArticles(path: path, sent: { success in - if !success { - DispatchQueue.main.async { - continuation.resume(returning: []) - } - return - } - }, reply: { articles in - DispatchQueue.main.async { - continuation.resume(returning: []) - } - }) + self?.client.sendGetNewsArticles(path: path) { articles in + continuation.resume(returning: []) + } } } @@ -484,8 +440,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { fullPath = Array(path[0..<path.count-1]) } - self.client.sendDownloadFile(name: fileName, path: fullPath, sent: { _ in - }, reply: { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in + self.client.sendDownloadFile(name: fileName, path: fullPath) { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in print("GOT DOWNLOAD REPLY:") print("\tSUCCESS?", success) print("\tTRANSFER SIZE: \(downloadTransferSize.debugDescription)") @@ -511,7 +466,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { fileClient.downloadToFile() } - }) + } } @MainActor func previewFile(_ fileName: String, path: [String], complete callback: ((PreviewFileInfo?) -> Void)? = nil) { @@ -520,13 +475,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { fullPath = Array(path[0..<path.count-1]) } - self.client.sendDownloadFile(name: fileName, path: fullPath, preview: true, sent: { success in - if !success { - callback?(nil) - return - } - - }, reply: { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in + self.client.sendDownloadFile(name: fileName, path: fullPath, preview: true) { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in guard success else { callback?(nil) return @@ -572,7 +521,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { } callback?(info) - }) + } } // @MainActor func previewFile(_ fileName: String, path: [String], addTransfer: Bool = false, complete callback: ((TransferInfo, Data) -> Void)? = nil) { @@ -669,12 +618,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { return } - self.client.sendDownloadBanner(sent: { success in - if !success { - print("FAIL BANNER") - return - } - }, reply: { [weak self] success, downloadReferenceNumber, downloadTransferSize in + self.client.sendDownloadBanner { [weak self] success, downloadReferenceNumber, downloadTransferSize in if !success { return } @@ -689,7 +633,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { self.bannerClient?.delegate = self self.bannerClient?.downloadToMemory() } - }) + } } // MARK: - Hotline Delegate @@ -744,8 +688,6 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { var existingUserIDs: [UInt] = [] var userList: [User] = [] - print("GOT USER LIST", users) - for u in users { if let i = self.users.firstIndex(where: { $0.id == u.id }) { // If a user is already in the user list we have to assume @@ -831,15 +773,12 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { } func hotlineFileDownloadedData(client: HotlineFileClient, reference: UInt32, data: Data) { - print("DOWNLOADED DATA?", reference) if let b = self.bannerClient, b.referenceNumber == reference { #if os(macOS) self.bannerImage = NSImage(data: data) #elseif os(iOS) self.bannerImage = UIImage(data: data) #endif - - print("DOWNLOADED BANNER!") } else if let i = self.transfers.firstIndex(where: { $0.id == reference }) { diff --git a/Hotline/Shared/NetSocket.swift b/Hotline/Shared/NetSocket.swift index e474f5a..5b33f61 100644 --- a/Hotline/Shared/NetSocket.swift +++ b/Hotline/Shared/NetSocket.swift @@ -41,6 +41,10 @@ final class NetSocket: NSObject, StreamDelegate { private var status: NetSocketStatus = .disconnected + @MainActor public func has(_ length: Int) -> Bool { + return (self.available >= length) + } + override init() {} @MainActor public func connect(host: String, port: Int) { @@ -59,14 +63,16 @@ final class NetSocket: NSObject, StreamDelegate { inputStream?.delegate = self outputStream?.delegate = self - inputStream?.schedule(in: .current, forMode: .common) - outputStream?.schedule(in: .current, forMode: .common) + inputStream?.schedule(in: .current, forMode: .default) + outputStream?.schedule(in: .current, forMode: .default) inputStream?.open() outputStream?.open() } @MainActor public func close(_ err: Error? = nil) { + print("NetSocket: Closed") + let disconnected = (self.status != .disconnected) self.status = .disconnected @@ -75,8 +81,8 @@ final class NetSocket: NSObject, StreamDelegate { self.output?.delegate = nil self.input?.close() self.output?.close() - self.input?.remove(from: .current, forMode: .common) - self.output?.remove(from: .current, forMode: .common) + self.input?.remove(from: .current, forMode: .default) + self.output?.remove(from: .current, forMode: .default) self.input = nil self.output = nil self.inputBuffer = [] @@ -85,8 +91,6 @@ final class NetSocket: NSObject, StreamDelegate { if disconnected { self.delegate?.netsocketDisconnected(socket: self, error: err) } - - print("NetSocket: Closed") } @MainActor public func write(_ data: Data) { @@ -166,6 +170,7 @@ final class NetSocket: NSObject, StreamDelegate { } let bytesWritten = output.write(self.outputBuffer, maxLength: self.outputBuffer.count) + print("NetSocket => \(bytesWritten) bytes") if bytesWritten > 0 { self.outputBuffer.removeFirst(bytesWritten) self.delegate?.netsocketSent(socket: self, count: bytesWritten) @@ -180,9 +185,10 @@ final class NetSocket: NSObject, StreamDelegate { return } - let bytesRead = input.read(&self.readBuffer, maxLength: self.readBuffer.capacity) + let bytesRead = input.read(&self.readBuffer, maxLength: 4 * 1024) + print("NetSocket <= \(bytesRead) bytes") if bytesRead > 0 { - self.inputBuffer.append(contentsOf: readBuffer[0..<bytesRead]) + self.inputBuffer.append(contentsOf: self.readBuffer[0..<bytesRead]) self.delegate?.netsocketReceived(socket: self, bytes: self.inputBuffer) } else if bytesRead == -1 { @@ -199,6 +205,9 @@ final class NetSocket: NSObject, StreamDelegate { switch eventCode { case .openCompleted: + if aStream == input { + self.setupStreamOptions() + } if input.streamStatus == .open && output.streamStatus == .open { if self.status == .connecting { print("NetSocket: Connected") @@ -224,4 +233,37 @@ final class NetSocket: NSObject, StreamDelegate { break } } + + // MARK: - + + private func setupStreamOptions() { + if let input = self.input { + let socketData: Data = CFReadStreamCopyProperty(input as CFReadStream, CFStreamPropertyKey.socketNativeHandle) as! Data; + var socketHandle: CFSocketNativeHandle = 0; + (socketData as NSData).getBytes(&socketHandle, length: MemoryLayout.size(ofValue: socketHandle)); + + var value: Int = 0; + let size = UInt32(MemoryLayout.size(ofValue: value)); + + value = 1; + if setsockopt(socketHandle, IPPROTO_TCP, TCP_NODELAY, &value, size) != 0 { + print("NetSocket: failed to set TCP_NODELAY"); + } + // Enable keepalive + value = 1; + if setsockopt(socketHandle, SOL_SOCKET, SO_KEEPALIVE, &value, size) != 0 { + print("NetSocket: failed to set SO_KEEPALIVE"); + } + // Number of keepalives before close (including first keepalive packet) + value = 5 + if setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPCNT, &value, size) != 0 { + print("NetSocket: failed to set TCP_KEEPCNT"); + } + // Idle time used when SO_KEEPALIVE is enabled. Sets how long connection must be idle before keepalive is sent. + value = 60 + if setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPALIVE, &value, size) != 0 { + print("NetSocket: failed to set TCP_KEEPALIVE") + } + } + } } diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift index edb8bee..a1df815 100644 --- a/Hotline/Utility/FoundationExtensions.swift +++ b/Hotline/Utility/FoundationExtensions.swift @@ -62,8 +62,26 @@ extension Array where Element == UInt8 { return val } + mutating func consume(_ length: Int) -> Bool { + guard length <= self.count else { + return false + } + + self.removeFirst(length) + return true + } + mutating func consumeBytes(_ length: Int) -> Data? { - guard let val = self.readData(at: 0, length: length) else { + guard let val: Data = self.readData(at: 0, length: length) else { + return nil + } + + self.removeFirst(length) + return val + } + + mutating func consumeBytes(_ length: Int) -> [UInt8]? { + guard let val: [UInt8] = self.readData(at: 0, length: length) else { return nil } @@ -130,17 +148,17 @@ extension Array where Element == UInt8 { return nil } - let leftSide: UInt64 = (UInt64(self[offset]) << 56) + + let a: UInt64 = (UInt64(self[offset]) << 56) + (UInt64(self[offset + 1]) << 48) + (UInt64(self[offset + 2]) << 40) + (UInt64(self[offset + 3]) << 32) - let rightSide: UInt64 = (UInt64(self[offset + 4]) << 24) + + let b: UInt64 = (UInt64(self[offset + 4]) << 24) + (UInt64(self[offset + 5]) << 16) + (UInt64(self[offset + 6]) << 8) + UInt64(self[offset + 7]) - return leftSide + rightSide + return a + b } func readDate(at offset: Int) -> Date? { @@ -165,8 +183,15 @@ extension Array where Element == UInt8 { return Data(self[offset..<(offset + length)]) } + func readData(at offset: Int, length: Int) -> [UInt8]? { + guard offset >= 0, offset + length <= self.count else { + return nil + } + return Array(self[offset..<(offset + length)]) + } + func readString(at offset: Int, length: Int) -> String? { - guard let subdata = self.readData(at: offset, length: length) else { + guard let subdata: Data = self.readData(at: offset, length: length) else { return nil } @@ -233,8 +258,8 @@ extension Array where Element == UInt8 { mutating func appendUInt16(_ value: UInt16, endianness: Endianness = .big) { let val = endianness == .big ? value.bigEndian : value.littleEndian let bytes: [UInt8] = [ - UInt8((val >> 8) & 0xFF), - UInt8(val & 0xFF) + UInt8(val & 0x00FF), + UInt8((val >> 8) & 0x00FF), ] self.append(contentsOf: bytes) } @@ -242,10 +267,10 @@ extension Array where Element == UInt8 { mutating func appendUInt32(_ value: UInt32, endianness: Endianness = .big) { let val = endianness == .big ? value.bigEndian : value.littleEndian let bytes: [UInt8] = [ - UInt8((val >> 24) & 0xFF), - UInt8((val >> 16) & 0xFF), - UInt8((val >> 8) & 0xFF), - UInt8(val & 0xFF) + UInt8(val & 0x000000FF), + UInt8((val >> 8) & 0x000000FF), + UInt8((val >> 16) & 0x000000FF), + UInt8((val >> 24) & 0x000000FF), ] self.append(contentsOf: bytes) } @@ -253,17 +278,25 @@ extension Array where Element == UInt8 { mutating func appendUInt64(_ value: UInt64, endianness: Endianness = .big) { let val: UInt64 = endianness == .big ? value.bigEndian : value.littleEndian let bytes: [UInt8] = [ - UInt8((val >> 56) & 0xFF), - UInt8((val >> 48) & 0xFF), - UInt8((val >> 40) & 0xFF), - UInt8((val >> 32) & 0xFF), - UInt8((val >> 24) & 0xFF), - UInt8((val >> 16) & 0xFF), - UInt8((val >> 8) & 0xFF), - UInt8(val & 0xFF) + UInt8(val & 0x00000000000000FF), + UInt8((val >> 8) & 0x00000000000000FF), + UInt8((val >> 16) & 0x00000000000000FF), + UInt8((val >> 24) & 0x00000000000000FF), + UInt8((val >> 32) & 0x00000000000000FF), + UInt8((val >> 40) & 0x00000000000000FF), + UInt8((val >> 48) & 0x00000000000000FF), + UInt8((val >> 56) & 0x00000000000000FF), ] self.append(contentsOf: bytes) } + + mutating func appendData(_ data: Data) { + self.append(contentsOf: data) + } + + mutating func appendData(_ data: [UInt8]) { + self.append(contentsOf: data) + } } extension Data { diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift index b21e4fb..7571bdf 100644 --- a/Hotline/macOS/ServerView.swift +++ b/Hotline/macOS/ServerView.swift @@ -448,7 +448,6 @@ struct ServerView: View { model.disconnect() } else { - print("GETTING USER LIST????!") sendPreferences() model.getUserList() model.downloadBanner() |