aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline/Hotline/HotlineClient.swift966
-rw-r--r--Hotline/Hotline/HotlineProtocol.swift119
-rw-r--r--Hotline/Hotline/HotlineTrackerClient.swift22
-rw-r--r--Hotline/Models/Hotline.swift111
-rw-r--r--Hotline/Shared/NetSocket.swift58
-rw-r--r--Hotline/Utility/FoundationExtensions.swift71
-rw-r--r--Hotline/macOS/ServerView.swift1
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()