diff options
| -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() |